Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA#37
Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA#37bharvey88 wants to merge 2 commits into
Conversation
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughAdds stable/beta OTA channel selection, HTTP-based firmware updates, beta-channel ESPHome builds, rolling beta-release publication, separate minimal/factory artifacts, and factory manifest selection for the web installer. ChangesOTA configuration and firmware delivery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Device
participant ESPHomeConfig
participant GitHubManifest
participant FirmwareRelease
Device->>ESPHomeConfig: select firmware channel or press Firmware Update
ESPHomeConfig->>GitHubManifest: set and fetch OTA manifest URL
GitHubManifest->>FirmwareRelease: resolve stable or beta assets
FirmwareRelease-->>Device: provide firmware update artifact
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
Integrations/ESPHome/BTN-1_Minimal.yaml (1)
8-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winComponent wiring is correct —
ota: platform: http_requestalongsideupdate: platform: http_requestsatisfies the documented prerequisite for the managed-update platform.One DRY nit: the hard-coded
source:URL (line 17) duplicates the value Core.yaml already computes as${stable_manifest_base}/firmware/manifest.json(i.e.${ota_stable_manifest}). Sinceapply_ota_sourceoverwrites this at boot anyway, referencing the substitution instead of the literal avoids a future drift bug ifstable_manifest_baseever changes.♻️ Suggested fix
update: - platform: http_request id: update_http_request name: Firmware Update - source: https://apolloautomation.github.io/BTN-1/firmware/manifest.json + source: "${ota_stable_manifest}"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Integrations/ESPHome/BTN-1_Minimal.yaml` around lines 8 - 21, Replace the hard-coded source URL in the update_http_request configuration with the existing ota_stable_manifest substitution used by Core.yaml. Keep the http_request update wiring and on_connect component.update behavior unchanged.Integrations/ESPHome/Core.yaml (2)
50-54: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winBoot-time
apply_ota_sourcetriggers a network fetch before Wi-Fi is connected.This step runs at priority -100, likely before Wi‑Fi association completes, and
apply_ota_source(lines 753-763) doesn't just set the URL — it also firescomponent.update: update_http_request, i.e. an actual manifest HTTP fetch. That fetch will typically fail (no network yet) and gets redundantly repeated bywifi.on_connectinBTN-1_Minimal.yaml/BTN-1.yaml. See the refactor suggestion on the script definition (lines 753-763) to separate "set URL" from "trigger fetch".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Integrations/ESPHome/Core.yaml` around lines 50 - 54, Remove the boot-time script execution of apply_ota_source from the priority -100 block so it cannot trigger an HTTP fetch before Wi-Fi connects. Preserve OTA source initialization through the Wi-Fi connection flow, and update the apply_ota_source script as needed to separate URL assignment from component.update: update_http_request.
737-763: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSplit the manifest refresh from the update fetch
set_source_url()exists onHttpRequestUpdate, so keep the URL switch inapply_ota_source, but movecomponent.update: update_http_requestinto the update button path; the boot-time call atIntegrations/ESPHome/Core.yaml:53should only select the manifest, not trigger a fetch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Integrations/ESPHome/Core.yaml` around lines 737 - 763, The apply_ota_source script should only switch the manifest URL via set_source_url() and must no longer call component.update on update_http_request. Move that component.update action into the firmware update button path, while preserving the boot-time invocation so it selects the correct manifest without triggering a fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build-beta.yml:
- Around line 48-91: Add workflow-level concurrency control for the beta
publishing workflow, using a stable group keyed to the beta workflow or branch
so overlapping runs cannot execute `publish-beta` uploads concurrently.
Configure the concurrency behavior to serialize runs while preserving the
intended latest-run handling, and leave the existing release upload steps
unchanged.
In `@Integrations/ESPHome/Core.yaml`:
- Around line 319-346: Replace the fixed 5-second delay in the Firmware Update
on_press flow with a wait for the manifest refresh initiated by
update_http_request to complete, using an explicit completion signal and a
bounded timeout. Ensure perform(true) runs only after refresh completion or
timeout, while preserving the existing deep-sleep rearming logic.
---
Nitpick comments:
In `@Integrations/ESPHome/BTN-1_Minimal.yaml`:
- Around line 8-21: Replace the hard-coded source URL in the update_http_request
configuration with the existing ota_stable_manifest substitution used by
Core.yaml. Keep the http_request update wiring and on_connect component.update
behavior unchanged.
In `@Integrations/ESPHome/Core.yaml`:
- Around line 50-54: Remove the boot-time script execution of apply_ota_source
from the priority -100 block so it cannot trigger an HTTP fetch before Wi-Fi
connects. Preserve OTA source initialization through the Wi-Fi connection flow,
and update the apply_ota_source script as needed to separate URL assignment from
component.update: update_http_request.
- Around line 737-763: The apply_ota_source script should only switch the
manifest URL via set_source_url() and must no longer call component.update on
update_http_request. Move that component.update action into the firmware update
button path, while preserving the boot-time invocation so it selects the correct
manifest without triggering a fetch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2533d63f-356a-4032-be56-b690282a5056
📒 Files selected for processing (8)
.github/workflows/build-beta.yml.github/workflows/build.ymlIntegrations/ESPHome/.gitignoreIntegrations/ESPHome/BTN-1.yamlIntegrations/ESPHome/BTN-1_Minimal.yamlIntegrations/ESPHome/Core.yamlIntegrations/ESPHome/beta-channel/BTN-1_Minimal.yamlstatic/index.html
| 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 BTN-1 beta firmware. Auto-updated on every push to the beta branch." | ||
|
|
||
| - name: Rewrite manifest to absolute URLs and upload assets | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" | ||
| man=$(find fw/firmware-beta -name manifest.json | head -1) | ||
| if [ -z "$man" ]; then | ||
| echo "::error::manifest.json not found" | ||
| 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. | ||
| jq --arg base "$BASE" ' | ||
| .builds[0].ota.path = ($base + "/" + (.builds[0].ota.path | sub(".*/"; ""))) | ||
| | .builds[0].parts |= map(.path = ($base + "/" + (.path | sub(".*/"; "")))) | ||
| ' "$man" > manifest.json | ||
| cat manifest.json | ||
| gh release upload beta-fw manifest.json -R "${{ github.repository }}" --clobber | ||
| find fw/firmware-beta -name '*.bin' -print -exec \ | ||
| gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; | ||
| echo "Beta assets published." |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Missing concurrency control on the rolling beta-fw release publish.
publish-beta uploads manifest.json and .bin assets to the same rolling beta-fw release with --clobber, and live devices on the Beta channel actively pull from these exact URLs. If two workflow runs overlap (e.g. rapid successive pushes to beta, or a manual workflow_dispatch racing a push-triggered run), their uploads can interleave, leaving the release with a manifest from one commit and binaries from another — devices checking for updates mid-publish could fetch a corrupted/mismatched pairing. Add a concurrency: group to serialize runs for this workflow.
🔒 Suggested fix
on:
push:
branches: [beta]
paths:
- 'Integrations/ESPHome/**'
workflow_dispatch:
+concurrency:
+ group: beta-firmware-release
+ cancel-in-progress: false
+
# Least privilege: read-only by default; only publish-beta is elevated to write.
permissions:
contents: read🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-beta.yml around lines 48 - 91, Add workflow-level
concurrency control for the beta publishing workflow, using a stable group keyed
to the beta workflow or branch so overlapping runs cannot execute `publish-beta`
uploads concurrently. Configure the concurrency behavior to serialize runs while
preserving the intended latest-run handling, and leave the existing release
upload steps unchanged.
| - 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(); | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== File outline ==\n'
wc -l Integrations/ESPHome/Core.yaml
sed -n '280,390p' Integrations/ESPHome/Core.yaml
printf '\n== Symbol search ==\n'
rg -n "apply_ota_source|update_http_request|ota_mode|prevent_sleep|deep_sleep_1|wait_until|perform\\(true\\)|ota.path|latest_version|manifest" Integrations/ESPHome/Core.yaml
printf '\n== Nearby referenced files ==\n'
git ls-files | rg '(^|/)(Core\.yaml|.*\.yaml|.*\.yml)$'Repository: ApolloAutomation/BTN-1
Length of output: 6141
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== apply_ota_source and update_http_request definition ==\n'
sed -n '730,790p' Integrations/ESPHome/Core.yaml
printf '\n== Search for update_http_request ==\n'
rg -n "id:\s*update_http_request|platform:\s*http_request|platform:\s*update|component.update:\s*update_http_request|set_source_url|latest_version|update_info" Integrations/ESPHome/Core.yaml Integrations/ESPHome/*.yaml
printf '\n== ESPHome docs / source clues in repo ==\n'
rg -n "component.update|http_request.*update|update entity|manifest" README.md Integrations/ESPHome -g '!**/*.png'Repository: ApolloAutomation/BTN-1
Length of output: 5113
🌐 Web query:
ESPHome http_request update platform component.update asynchronous manifest fetch source_url update_info semantics
💡 Result:
The ESPHome http_request update platform allows devices to automatically check for and install firmware updates by fetching a JSON manifest file [1][2]. Asynchronous Execution The manifest fetch and update checking process is executed in a dedicated background task (specifically on ESP32-based devices) [3][4]. This asynchronous design ensures that the main ESPHome loop is not blocked during HTTP operations, which prevents device watchdog timeouts and allows the system to yield appropriately while waiting for network responses [4]. Manifest Fetch and Semantics The component uses the source_url configuration variable to locate the YAML/JSON manifest file [2][5]. The semantics of the manifest processing are as follows: - The component periodically checks the source_url for updates, with an configurable update_interval (default is 6 hours) [2]. - Network connectivity checks are performed before attempting the fetch; recent improvements ensure that if the network is not connected, the update check is skipped silently to avoid unnecessary error logs during startup [6]. - Once fetched, the manifest (expected to be in ESP-Web-Tools format) is parsed to extract firmware metadata [2]. - Paths within the manifest (such as the firmware binary location) are resolved relative to the source_url or as absolute URLs based on specific prefixes: - URLs starting with http/https are treated as absolute [2]. - Paths starting with a forward slash (/) are appended to the manifest's source hostname [2]. - Other paths are appended to the source_url path after stripping the manifest filename [2]. Internal Handling The HttpRequestUpdate class handles this workflow by managing an update_task [7]. Upon triggering a check, it retrieves the manifest, validates it, and updates internal state (such as update_info_) by deferring the final data write back to the main loop to ensure thread safety [3][8]. The actual installation process is coordinated with the OTA Update via HTTP Request component [2][3].
Citations:
- 1: https://esphome.io/components/update/http_request/
- 2: https://new.esphome.io/components/update/http_request/
- 3: https://api-docs-beta.esphome.io/http__request__update_8cpp_source
- 4: [http_request] Bugfix: run update function in a task esphome/esphome#8018
- 5: https://api-docs.esphome.io/http__request__update_8h_source
- 6: [http_request] Skip update check when network not connected esphome/esphome#12418
- 7: https://api-docs.esphome.io/classesphome_1_1http__request_1_1_http_request_update
- 8: https://api-docs.esphome.io/http__request__update_8cpp_source
🌐 Web query:
ESPHome http_request update entity available state update_info latest_version perform(true) semantics
💡 Result:
In ESPHome, the interaction between http_request and the update entity (specifically the http_request platform for updates) involves managing firmware metadata and deployment states [1][2][3]. The update_info_ structure and perform(bool force) method are central to how these updates function [3][4]: 1. update_info_: This internal member holds metadata about the available update, including fields such as latest_version, title, firmware_url, and md5 [3][5]. When the component checks for updates (typically by reading a JSON manifest), it parses these details and populates the update_info_ structure [1][3]. 2. perform(bool force): This method is used to initiate the installation process [3][4]. - When called, the component checks the current state of the update entity [3]. - Unless the force parameter is set to true, the component will generally only proceed if the state is UPDATE_STATE_AVAILABLE [3]. - If triggered, it sets the entity state to UPDATE_STATE_INSTALLING, configures the parent OTA component with the firmware URL and MD5 hash extracted from update_info_, and begins the download and installation [3]. 3. Semantics: The perform() action is defined in the base UpdateEntity class and implemented by HttpRequestUpdate [4]. The latest_version is updated during the background check task as part of the UpdateInfo object, which is then moved into update_info_ on the main execution loop to ensure thread safety when updating the entity's state [3]. The update() method handles the periodic check for new versions, while perform() handles the execution of the update process itself [3][6].
Citations:
- 1: https://esphome.io/components/update/http_request/
- 2: https://api-docs.esphome.io/classesphome_1_1http__request_1_1_http_request_update
- 3: https://api-docs.esphome.io/http__request__update_8cpp_source
- 4: https://api-docs.esphome.io/classesphome_1_1update_1_1_update_entity
- 5: https://api-docs.esphome.io/update__entity_8cpp_source
- 6: https://api-docs.esphome.io/http__request__update_8h_source
Replace the fixed 5s sleep with a real refresh-complete check.
component.update: update_http_request refreshes the manifest in the background, so this blind delay can still reach perform(true) while the previous manifest is active. Wait on a concrete completion signal with a timeout before forcing the install.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/Core.yaml` around lines 319 - 346, Replace the fixed
5-second delay in the Firmware Update on_press flow with a wait for the manifest
refresh initiated by update_http_request to complete, using an explicit
completion signal and a bounded timeout. Ensure perform(true) runs only after
refresh completion or timeout, while preserving the existing deep-sleep rearming
logic.
…l OTA Rebuild of the earlier firmware-channel port on the pattern MSR-1 shipped as 26.7.9.1 (ApolloAutomation/MSR-1 #100/#103/#104): - NEW: bluetooth_proxy + esp32_ble_tracker compile into every image; a "Bluetooth Proxy" switch (default off, persisted, re-applied at boot) starts/stops scanning at runtime. On this deep-sleep battery button the proxy pairs with "Prevent Sleep" for continuous use. - Firmware Channel select (Stable/Beta) + apply_ota_source doing a direct set_source_url swap from ota_stable_manifest/ota_beta_manifest subs; the channel is re-applied at boot (on_boot -100) - Firmware Update button holds prevent_deep_sleep for the download and re-arms sleep on the not-started path; no pre-OTA BLE disable (ESPHome's OTA quiesces BLE itself) - http_request consolidated into Core.yaml with the proven buffer sizes (rx 5120 / tx 2048 for GitHub release redirects) - BTN-1_Minimal gains the managed-update machinery and takes over Pages firmware/ for OTA/adoption; the improv image moves to firmware-factory/ (installer page repointed) - beta-channel/ wrapper defaults the select to Beta; build-beta.yml publishes the manifest (absolute URLs) to the rolling beta-fw pre-release - min_version 2025.11.0 (update-system floor); version 26.7.12.1 Supersedes #36. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
478b57f to
cc47aaa
Compare
|
Updated: per fleet policy every product now gets the runtime Bluetooth Proxy switch, so this PR adds bluetooth_proxy + esp32_ble_tracker to Core with the standard switch (default off, needs Prevent Sleep to be useful on this battery device). Branch force-pushed; body updated. |
Add an empty encryption: key to the api: block so ESPHome/HA provisions a per-device API key on adoption, matching MSR-1. Bump firmware version to 26.7.14.1. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Version: 26.7.12.1
What does this implement/fix?
Rebuild of #36 on the unified pattern MSR-1 shipped as 26.7.9.1 (ApolloAutomation/MSR-1#100, ApolloAutomation/MSR-1#103, ApolloAutomation/MSR-1#104). Everything #36 did, plus the fix set that landed on MSR-1 afterward:
bluetooth_proxy+esp32_ble_trackernow compile into every image with a "Bluetooth Proxy" switch (default off, persisted, re-applied at boot). BTN-1 never had a BLE proxy offering; the C6/8MB has the headroom, and the switch defaults off so nothing changes until a user opts in. On this deep-sleep battery button the proxy only forwards while awake - pair with "Prevent Sleep" (and expect the battery cost).apply_ota_source: a directset_source_urlswap fromota_stable_manifest/ota_beta_manifestsubstitutions. The channel is now also re-applied at boot (on_boot -100), so a rebooted device keeps pointing at its selected channel.prevent_deep_sleep()for the download and re-arms sleep on the not-started path (unlessota_mode/Prevent Sleep holds it awake). The#ifdef USE_ESP32_BLEdisable/re-enable lambdas from Add Firmware Channel switching and serve the end-user image for OTA #36 are gone — ESPHome's OTA quiesces BLE itself (this is MSR-1 #103).http_requestconsolidated into Core.yaml withbuffer_size_rx: 5120/buffer_size_tx: 2048— GitHub release redirects overflow the 512-byte defaults (~3.6 KB CSP header line, ~850-char signed query string). Add Firmware Channel switching and serve the end-user image for OTA #36 had no buffer sizing; beta-channel OTA fails without it.firmware/; the improv image moves tofirmware-factory/;static/index.htmlrepointed. install.apolloautomation.com needs the matching repoint when this ships to main (mirrors Point MSR-1 installer at the factory image (restores Wi-Fi setup step) installer#16).beta-channel/wrapper + build-beta.yml publishing to the rollingbeta-fwpre-release.min_version2025.6.0 → 2025.11.0 (the update-system floor set on R_PRO-1); version 26.7.12.1.Not touched here (pre-existing, worth separate attention): weekly.yml has a dangling empty matrix entry, so its second job builds nothing.
Supersedes #36.
Types of changes
Checklist / Checklijst:
If user-visible functionality or configuration variables are added/modified:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Improvements