-
Notifications
You must be signed in to change notification settings - Fork 15
Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Changes from all commits
fe2eab8
217e387
9e86edc
ca41ea4
005c4f1
49bda7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| name: Build and Publish Beta | ||
|
|
||
| # Builds TEMP-1 firmware from the beta branch and publishes it as assets on a | ||
| # rolling "beta-fw" pre-release. The on-device "Firmware Channel" select points | ||
| # OTA updates at these assets. Stable firmware is built/published separately | ||
| # by build.yml (push to main -> GitHub Pages). | ||
|
|
||
| on: | ||
| push: | ||
| branches: [beta] | ||
| paths: | ||
| - 'Integrations/ESPHome/**' | ||
| workflow_dispatch: | ||
|
|
||
| # Least privilege: read-only by default; only publish-beta is elevated to write. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| version: | ||
| name: Read version | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| v: ${{ steps.read.outputs.v }} | ||
| steps: | ||
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
| with: | ||
| persist-credentials: false | ||
| - id: read | ||
| run: | | ||
| v=$(awk '/substitutions:/ {f=1} f && /version:/ {print $2; exit}' \ | ||
| Integrations/ESPHome/Core.yaml | tr -d '"') | ||
| echo "v=$v" >> "$GITHUB_OUTPUT" | ||
| echo "Beta version: $v" | ||
|
|
||
| build: | ||
| name: Build ${{ matrix.name }} | ||
| needs: version | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| # Beta serves OTA updates only, so it builds the end-user Minimal | ||
| # images, not the first-flash improv images. Artifact names match | ||
| # the on-device variant_slug: "", "-b", "2", "-b2". | ||
| - { yaml: Integrations/ESPHome/beta-channel/TEMP-1_Minimal.yaml, name: firmware-beta } | ||
| - { yaml: Integrations/ESPHome/beta-channel/TEMP-1B_Minimal.yaml, name: firmware-b-beta } | ||
| - { yaml: Integrations/ESPHome/beta-channel/TEMP-1_Minimal_R2.yaml, name: firmware2-beta } | ||
| - { yaml: Integrations/ESPHome/beta-channel/TEMP-1B_Minimal_R2.yaml, name: firmware-b2-beta } | ||
| uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 | ||
| with: | ||
| files: ${{ matrix.yaml }} | ||
| esphome-version: stable | ||
| combined-name: ${{ matrix.name }} | ||
| release-version: ${{ needs.version.outputs.v }} | ||
|
|
||
| publish-beta: | ||
| name: Publish beta release assets | ||
| needs: [version, build] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - name: Download firmware artifacts | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| path: fw | ||
| pattern: firmware* | ||
|
|
||
| - name: Ensure rolling 'beta-fw' pre-release exists | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh release view beta-fw -R "${{ github.repository }}" >/dev/null 2>&1 \ | ||
| || gh release create beta-fw -R "${{ github.repository }}" \ | ||
| --prerelease --title "Beta (rolling)" \ | ||
| --notes "Latest TEMP-1 beta firmware. Auto-updated on every push to the beta branch." | ||
|
|
||
| - name: Rewrite manifests to absolute URLs and upload assets | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" | ||
| # Manifest names match the on-device variant_slug: "", "-b", "2", "-b2". | ||
| declare -A DIRS=( [""]=firmware-beta ["-b"]=firmware-b-beta ["2"]=firmware2-beta ["-b2"]=firmware-b2-beta ) | ||
| for v in "" "-b" "2" "-b2"; do | ||
| src="fw/${DIRS[$v]}" | ||
| man=$(find "$src" -name manifest.json | head -1) | ||
| if [ -z "$man" ]; then | ||
| echo "::error::manifest.json not found for ${DIRS[$v]}" | ||
| exit 1 | ||
| fi | ||
| echo "Rewriting $man" | ||
| # Make ota.path and parts[].path absolute release-asset URLs so the | ||
| # device never has to resolve a relative path against a redirect. | ||
| # The four variants have distinct device names, so bin filenames | ||
| # don't collide in the flat release-asset namespace. | ||
| jq --arg base "$BASE" ' | ||
| .builds[0].ota.path = ($base + "/" + (.builds[0].ota.path | sub(".*/"; ""))) | ||
| | .builds[0].parts |= map(.path = ($base + "/" + (.path | sub(".*/"; "")))) | ||
| ' "$man" > "manifest$v.json" | ||
| cat "manifest$v.json" | ||
| gh release upload beta-fw "manifest$v.json" -R "${{ github.repository }}" --clobber | ||
| find "$src" -name '*.bin' -print -exec \ | ||
| gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; | ||
| done | ||
| echo "Beta assets published." | ||
|
|
||
| - name: Point beta-fw tag at the built commit | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| # gh release create tags default-branch HEAD, and uploads never move | ||
| # the tag, so without this the release's source commit drifts away | ||
| # from the assets actually published. | ||
| gh api -X PATCH "repos/${{ github.repository }}/git/refs/tags/beta-fw" \ | ||
| -f sha="${{ github.sha }}" -F force=true | ||
| echo "beta-fw -> ${{ github.sha }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ | |
| # You can modify this file to suit your needs. | ||
| /.esphome/ | ||
| /secrets.yaml | ||
| beta-channel/.esphome/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,20 @@ | ||
| substitutions: | ||
| version: "26.8.27.1" | ||
| # Firmware variant identity: overridden by the TEMP-1B / R2 images so each | ||
| # hardware variant tracks its own manifests ("", "-b", "2", "-b2"). | ||
| variant_slug: "" | ||
| # Default update channel on first boot (no stored user choice yet, i.e. a | ||
| # fresh flash). The beta-channel builds override this to "Beta" (see | ||
| # Integrations/ESPHome/beta-channel/) so firmware obtained from the beta | ||
| # channel keeps tracking it instead of offering a stable "downgrade". | ||
| firmware_channel_default: "Stable" | ||
| # Manifest URL bases. Stable = GitHub Pages (main branch builds). | ||
| # Beta = rolling "beta-fw" pre-release assets (beta branch builds). | ||
| stable_manifest_base: "https://apolloautomation.github.io/TEMP-1" | ||
| beta_manifest_base: "https://github.com/ApolloAutomation/TEMP-1/releases/download/beta-fw" | ||
| # Per-variant OTA manifest URLs picked by apply_ota_source. | ||
| ota_stable_manifest: "${stable_manifest_base}/firmware${variant_slug}/manifest.json" | ||
| ota_beta_manifest: "${beta_manifest_base}/manifest${variant_slug}.json" | ||
|
|
||
| esp32: | ||
| variant: esp32c3 | ||
|
|
@@ -10,13 +25,39 @@ esp32: | |
| assertion_level: SILENT | ||
| enable_lwip_assert: false | ||
|
|
||
| esp32_ble_tracker: | ||
| id: ble_tracker | ||
| scan_parameters: | ||
| continuous: true | ||
|
|
||
| bluetooth_proxy: | ||
|
|
||
| esphome: | ||
| # List form so package merging concatenates with each variant's own on_boot | ||
| # entries (mapping form would be replaced by the variant's block instead). | ||
| on_boot: | ||
| priority: 500 | ||
| then: | ||
| - script.execute: configureSourceSensorPolling | ||
| - priority: 500 | ||
| then: | ||
| - script.execute: configureSourceSensorPolling | ||
| # Point the update entity at the selected channel's manifest. | ||
| - priority: -100 | ||
| then: | ||
| - script.execute: apply_ota_source | ||
| # Re-apply the Bluetooth Proxy switch after all components set up, so BLE | ||
| # scanning matches the persisted switch (proxy stays off by default). | ||
| - priority: -300 | ||
| then: | ||
| - if: | ||
| condition: | ||
| switch.is_on: bluetooth_proxy_switch | ||
| then: | ||
| - esp32_ble_tracker.start_scan: | ||
| continuous: true | ||
| else: | ||
| - esp32_ble_tracker.stop_scan: | ||
|
|
||
| api: | ||
| encryption: | ||
| actions: | ||
| - action: play_buzzer | ||
| variables: | ||
|
|
@@ -74,6 +115,17 @@ globals: | |
|
|
||
| captive_portal: | ||
|
|
||
| http_request: | ||
| verify_ssl: true | ||
| # GitHub release-asset downloads answer with a redirect carrying a | ||
| # ~3.6 KB Content-Security-Policy header; each header line must fit | ||
| # this buffer or the request fails with "HTTP_CLIENT: Out of buffer". | ||
| buffer_size_rx: 5120 | ||
| # The redirect target is a signed URL with a ~850-char query string; the | ||
| # follow-up request line must fit the TX buffer or esp_http_client_open | ||
| # fails with "Out of buffer" before sending anything. | ||
| buffer_size_tx: 2048 | ||
|
|
||
|
|
||
| i2c: | ||
| sda: GPIO1 | ||
|
|
@@ -404,13 +456,56 @@ button: | |
| icon: mdi:power-cycle | ||
| name: "ESP Reboot" | ||
|
|
||
| - platform: template | ||
| name: "Firmware Update" | ||
| id: update_firmware | ||
| icon: mdi:cloud-download | ||
| entity_category: "config" | ||
| on_press: | ||
| - logger.log: "Applying firmware update for the selected channel" | ||
| # OTA download needs the device awake for its whole duration. | ||
| - lambda: |- | ||
| id(deep_sleep_1).prevent_deep_sleep(); | ||
| - delay: 3s | ||
| - script.execute: apply_ota_source | ||
| - script.wait: apply_ota_source | ||
| # The manifest fetch runs in its own task; give it a fixed window to land | ||
| # (update.is_available stays false for same-version switches). | ||
| - delay: 5s | ||
| - lambda: id(update_http_request).perform(true); | ||
| # Only reached if the update did not start (e.g. manifest unreachable). | ||
| # Re-arm deep sleep unless something else is holding the device awake. | ||
| - if: | ||
| condition: | ||
| and: | ||
| - switch.is_off: prevent_sleep | ||
| - binary_sensor.is_off: ota_mode | ||
| then: | ||
| - lambda: |- | ||
| id(deep_sleep_1).allow_deep_sleep(); | ||
|
Comment on lines
+459
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target section ---'
sed -n '400,510p' Integrations/ESPHome/Core.yaml
printf '%s\n' '--- related identifiers ---'
rg -n -C 4 'update_http_request|apply_ota_source|ota_mode|prevent_sleep|deep_sleep_1|component\.update|update_info|perform\(true\)' Integrations/ESPHome/Core.yaml
printf '%s\n' '--- repository references ---'
rg -n -C 3 'update_http_request|apply_ota_source|Firmware Update|ota_mode' .Repository: ApolloAutomation/TEMP-1 Length of output: 30064 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Core.yaml update declaration and setup hooks ---'
sed -n '1,110p' Integrations/ESPHome/Core.yaml
sed -n '600,635p' Integrations/ESPHome/Core.yaml
printf '%s\n' '--- all update/http_request declarations in tracked YAML ---'
rg -n -C 5 '^(update|http_request):|platform: http_request|on_progress|on_error|on_end|on_update|source:' Integrations/ESPHome -g '*.yaml'
printf '%s\n' '--- local ESPHome source or documentation references ---'
find . -type f \( -iname '*http*request*' -o -iname '*update*' \) -not -path '*/node_modules/*' | head -100
rg -n -C 3 'class.*HttpRequest|perform\(bool|set_source_url|component.update|update_info' . -g '*.{h,cc,cpp,py,md,yaml}'Repository: ApolloAutomation/TEMP-1 Length of output: 18983 🌐 Web query:
💡 Result: In ESPHome, the HttpRequestUpdate component manages firmware updates via an HTTP manifest file [1]. The perform(bool) method is a core function of this component, used to execute the firmware update process [2][3]. Source Code Implementation The perform(bool) method is defined within the HttpRequestUpdate class (namespace esphome::http_request) [2][4]. The implementation logic is as follows [2][5]: void HttpRequestUpdate::perform(bool force) { if (this->state_!= update::UPDATE_STATE_AVAILABLE &&!force) { return; } this->state_ = update::UPDATE_STATE_INSTALLING; this->publish_state; this->ota_parent_->set_md5(this->update_info.md5); this->ota_parent_->set_url(this->update_info.firmware_url); // Flash in the next loop this->defer([this] { this->ota_parent_->flash; }); } Key Details: - Purpose: This method initiates the installation of an available update [2]. - Logic: It checks if an update is available (state_ == update::UPDATE_STATE_AVAILABLE) unless the force flag is set to true [2][6]. - Process: If permitted, it transitions the state to UPDATE_STATE_INSTALLING, updates the OTA parent component with the firmware metadata (MD5 and URL), and triggers the flash process via a deferred task to ensure safe execution in the main loop [2][6]. - Interaction: The HttpRequestUpdate class inherits from update::UpdateEntity [4], which provides the standard perform interface for ESPHome update components [7]. The component uses the update_info structure (containing metadata like firmware_url, md5, and version) to manage the update lifecycle [2][6]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import urlopen
from urllib.error import HTTPError
urls = [
"https://raw.githubusercontent.com/esphome/esphome/dev/esphome/components/http_request/http_request_update.cpp",
"https://raw.githubusercontent.com/esphome/esphome/dev/esphome/components/http_request/http_request_update.h",
"https://raw.githubusercontent.com/esphome/esphome/dev/esphome/components/deep_sleep/deep_sleep_component.cpp",
"https://raw.githubusercontent.com/esphome/esphome/dev/esphome/core/automation.cpp",
]
for url in urls:
print(f"--- {url} ---")
try:
text = urlopen(url, timeout=15).read().decode()
except Exception as exc:
print(f"FETCH_ERROR: {exc}")
continue
lines = text.splitlines()
needles = ("perform(", "defer(", "void HttpRequestUpdate::update",
"DeepSleepComponent::", "allow_deep_sleep", "prevent_deep_sleep",
"ComponentUpdateAction")
for i, line in enumerate(lines, 1):
if any(n in line for n in needles):
lo, hi = max(1, i - 8), min(len(lines), i + 18)
print("\n".join(f"{n:4}: {lines[n-1]}" for n in range(lo, hi + 1)))
print()
PYRepository: ApolloAutomation/TEMP-1 Length of output: 1194 🏁 Script executed: #!/bin/bash
set -eu
for path in \
esphome/components/http_request/http_request_update.cpp \
esphome/components/http_request/http_request_update.h \
esphome/components/deep_sleep/deep_sleep_component.cpp \
esphome/core/automation.cpp
do
url="https://raw.githubusercontent.com/esphome/esphome/dev/$path"
echo "--- $url ---"
body="$(curl -kfsSL --max-time 20 "$url")" || { echo "FETCH_ERROR"; continue; }
printf '%s\n' "$body" | nl -ba | awk '
/perform\(/ || /HttpRequestUpdate::update/ || /defer\(/ ||
/allow_deep_sleep/ || /prevent_deep_sleep/ || /ComponentUpdateAction/ {
start=NR-8; if (start<1) start=1; end=NR+18
for (i=start; i<=end; i++) want[i]=1
}
{ lines[NR]=$0 }
END {
for (i=1; i<=NR; i++) if (want[i]) printf "%4d: %s\n", i, lines[i]
}'
doneRepository: ApolloAutomation/TEMP-1 Length of output: 975 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from urllib.request import urlopen
import ssl
ctx = ssl._create_unverified_context()
paths = [
"esphome/components/http_request/http_request_update.cpp",
"esphome/components/http_request/http_request_update.h",
"esphome/components/deep_sleep/deep_sleep_component.cpp",
"esphome/core/automation.cpp",
]
needles = (
"perform(", "HttpRequestUpdate::update", "defer(",
"allow_deep_sleep", "prevent_deep_sleep", "ComponentUpdateAction",
)
for path in paths:
url = "https://raw.githubusercontent.com/esphome/esphome/dev/" + path
print("---", url, "---")
try:
lines = urlopen(url, timeout=20, context=ctx).read().decode().splitlines()
except Exception as exc:
print("FETCH_ERROR:", exc)
continue
selected = set()
for i, line in enumerate(lines):
if any(needle in line for needle in needles):
selected.update(range(max(0, i - 8), min(len(lines), i + 19)))
for i in sorted(selected):
print(f"{i+1:4}: {lines[i]}")
PYRepository: ApolloAutomation/TEMP-1 Length of output: 1125 🌐 Web query:
💡 Result: In the context of the ESPHome codebase, specifically within the Citations: 🌐 Web query:
💡 Result: In ESPHome, there is no single class named ComponentUpdateAction that provides a play method. Instead, the update component system uses specific automation actions—primarily PerformAction and CheckAction—to handle update-related tasks [1][2][3]. The play method is a standard member function required by the ESPHome action framework, which executes the action when called [4][5][2]. Key automation classes in the esphome::update namespace include: 1. PerformAction: Triggers an update process [2]. Its play method calls the underlying update entity's perform function (optionally with a force flag) [2][6]. 2. CheckAction: Triggers a check for available updates [2]. Its play method calls the update entity's check function [2][6]. If you encountered a reference to something like UpdateComponentAction, it is likely a internal class or a generic template used within the ESPHome codebase [4][7]. For standard YAML configuration automation, you would typically use the update.perform or update.check actions, which map to these internal automation classes [2][3]. Citations:
Keep the deep-sleep lock until the OTA attempt completes.
🤖 Prompt for AI Agents |
||
|
|
||
| - platform: factory_reset | ||
| disabled_by_default: True | ||
| name: "Factory Reset ESP" | ||
| id: factory_reset_all | ||
|
|
||
|
|
||
| select: | ||
| - platform: template | ||
| name: "Firmware Channel" | ||
| id: firmware_channel | ||
| icon: mdi:source-branch | ||
| entity_category: "config" | ||
| optimistic: true | ||
| restore_value: true | ||
| options: | ||
| - "Stable" | ||
| - "Beta" | ||
| initial_option: "${firmware_channel_default}" | ||
| on_value: | ||
| then: | ||
| - script.execute: apply_ota_source | ||
|
|
||
| - platform: template | ||
| name: "Select Probe" | ||
| optimistic: true | ||
|
|
@@ -487,6 +582,21 @@ switch: | |
| entity_category: "config" | ||
| disabled_by_default: true | ||
|
|
||
| # Note: on this deep-sleep device the proxy only forwards while the device | ||
| # is awake - pair it with "Prevent Sleep" to use it continuously. | ||
| - platform: template | ||
| name: "Bluetooth Proxy" | ||
| id: bluetooth_proxy_switch | ||
| icon: mdi:bluetooth | ||
| entity_category: "config" | ||
| restore_mode: RESTORE_DEFAULT_OFF | ||
| optimistic: true | ||
| on_turn_on: | ||
| - esp32_ble_tracker.start_scan: | ||
| continuous: true | ||
| on_turn_off: | ||
| - esp32_ble_tracker.stop_scan: | ||
|
|
||
|
|
||
| text_sensor: | ||
| - platform: wifi_info | ||
|
|
@@ -505,6 +615,18 @@ text_sensor: | |
| entity_category: "diagnostic" | ||
|
|
||
| script: | ||
| - id: apply_ota_source | ||
| # Sets the OTA manifest URL from the Firmware Channel select (Stable/Beta). | ||
| # The manifest URLs are per-variant (ota_*_manifest substitutions via | ||
| # ${variant_slug}) so each hardware variant tracks its own manifests. | ||
| then: | ||
| - lambda: |- | ||
| const bool beta = id(firmware_channel).current_option() == "Beta"; | ||
| std::string url = beta ? "${ota_beta_manifest}" : "${ota_stable_manifest}"; | ||
| ESP_LOGI("firmware", "OTA manifest set to: %s", url.c_str()); | ||
| id(update_http_request).set_source_url(url); | ||
| - component.update: update_http_request | ||
|
|
||
| - id: configureSourceSensorPolling | ||
| then: | ||
| - if: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Serialize publication to the rolling release.
Each
betapush can publish to the samebeta-fwrelease concurrently. A slower older run can overwrite assets and move the tag after a newer run completes. Add a workflow-level concurrency group with cancellation.Proposed fix
on: push: branches: [beta] paths: - 'Integrations/ESPHome/**' workflow_dispatch: +concurrency: + group: beta-fw-release + cancel-in-progress: true + permissions: contents: read🤖 Prompt for AI Agents