Add Firmware Channel switching from HA#89
Conversation
Same Stable/Beta channel switching as CAST-1 (ApolloAutomation/CAST-1#43 naming): - Firmware Channel select (Stable/Beta) sets the OTA manifest URL via a new apply_ota_source script; each image tracks its own variant's manifests through the ble_firmware substitution. - Firmware Update button force-installs the selected channel's firmware. - build-beta.yml publishes beta builds to a rolling beta pre-release. - build.yml now builds MSR-1.yaml as firmware/ so updates serve the end-user image; the Factory image moves to firmware-factory/ for the web installer only. Version: 26.7.7.1 🤖 Generated with [Claude Code](https://claude.com/claude-code)
WalkthroughAdds a new ChangesBeta Build Pipeline and OTA Channel Support
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant CoreYaml
participant ESPHomeBuildWorkflow
participant BetaPrerelease
GitHubActions->>CoreYaml: extract version via awk
CoreYaml-->>GitHubActions: version output
GitHubActions->>ESPHomeBuildWorkflow: build MSR-1 and MSR-1_BLE with version
ESPHomeBuildWorkflow-->>GitHubActions: manifest.json and .bin artifacts
GitHubActions->>GitHubActions: rewrite manifest.json URLs via jq
GitHubActions->>GitHubActions: rename .bin files with standard-/ble- prefix
GitHubActions->>BetaPrerelease: create or reuse beta prerelease
GitHubActions->>BetaPrerelease: upload manifests and binaries
sequenceDiagram
participant User
participant FirmwareChannelSelect
participant ApplyOtaSourceScript
participant UpdateHttpRequest
participant Device
User->>FirmwareChannelSelect: choose Stable or Beta
FirmwareChannelSelect->>ApplyOtaSourceScript: run apply_ota_source
ApplyOtaSourceScript->>UpdateHttpRequest: set manifest URL based on ble_firmware and channel
ApplyOtaSourceScript->>UpdateHttpRequest: component.update
User->>Device: press Firmware Update button
Device->>ApplyOtaSourceScript: run apply_ota_source
Device->>Device: disable BLE if USE_ESP32_BLE
Device->>UpdateHttpRequest: update.perform (force_update)
Device->>Device: re-enable BLE if applicable
Possibly related PRs
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: 1
🤖 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 `@Integrations/ESPHome/Core.yaml`:
- Around line 665-699: The firmware update flow unconditionally re-enables BLE
in the trailing lambda even when it may not have been disabled by this action.
In the update action sequence under the “Firmware Update” template, capture
whether `esp32_ble::global_ble` was active before disabling it, then only call
`enable()` in the fallback path when this step actually turned BLE off. Keep the
state check and restore logic paired with the existing disable/enable lambdas so
the original BLE state is preserved.
🪄 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: 0e29470a-bfdc-40e8-9a75-66e8de1fcb0c
📒 Files selected for processing (5)
.github/workflows/build-beta.yml.github/workflows/build.ymlIntegrations/ESPHome/Core.yamlIntegrations/ESPHome/MSR-1_BLE.yamlstatic/index.html
| - platform: template | ||
| name: "Firmware Update" | ||
| id: update_firmware | ||
| icon: mdi:cloud-download | ||
| entity_category: "config" | ||
| on_press: | ||
| - logger.log: "Applying firmware based on selected channel and variant" | ||
| # Free heap for the TLS download on the BLE variant. Guarded so the | ||
| # non-BLE variant compiles the same YAML. | ||
| - lambda: |- | ||
| #ifdef USE_ESP32_BLE | ||
| if (esp32_ble::global_ble && esp32_ble::global_ble->is_active()) { | ||
| ESP_LOGI("firmware", "Disabling BLE for firmware update"); | ||
| esp32_ble::global_ble->disable(); | ||
| } | ||
| #endif | ||
| - delay: 3s | ||
| - script.execute: apply_ota_source | ||
| - script.wait: apply_ota_source | ||
| # The manifest fetch runs in its own task and YAML has no "fetch done" | ||
| # condition to wait on (update.is_available stays false for same-version | ||
| # variant switches), so give it a fixed window like R_PRO-1/CAST-1 do. | ||
| - delay: 5s | ||
| - update.perform: | ||
| id: update_http_request | ||
| force_update: true | ||
| # Only reached if the update did not start (e.g. manifest unreachable). | ||
| - lambda: |- | ||
| #ifdef USE_ESP32_BLE | ||
| if (esp32_ble::global_ble) { | ||
| ESP_LOGI("firmware", "Re-enabling BLE (no update performed)"); | ||
| esp32_ble::global_ble->enable(); | ||
| } | ||
| #endif | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
ESPHome update.perform http_request blocking or async behavior
💡 Result:
In ESPHome, the behavior of http_request and its associated update mechanism involves important distinctions between blocking and asynchronous execution to ensure system stability. HTTP Request Component The underlying http_request component has historically been known for its blocking nature, as it often relies on underlying libraries (like C++ httplib or standard Arduino WiFiClient) that wait for the full response before proceeding [1][2][3]. This behavior can block the main execution loop if a request takes too long, potentially causing issues like lost data or system instability [1]. Efforts are ongoing within the ESPHome project to move these operations toward a more asynchronous execution model to prevent blocking the main loop [1]. update.perform Action When using the update.perform action (specifically in the context of an http_request update source), the behavior is designed to avoid blocking the system during the actual firmware installation [4]. 1. Initiation: When perform is called, the component validates the update state [4]. 2. Deferred Execution: Rather than performing the heavy lifting of the OTA (Over-the-Air) flash process immediately and synchronously, the implementation uses a deferred execution pattern [4]. The flashing process is scheduled to occur in the next loop cycle using a defer call, ensuring the current execution context is not stalled by the initiation of the update [4]. Summary While standard http_request send actions can be blocking in their communication phase, the update.perform action is specifically implemented to be non-blocking by deferring the heavy flash operation to subsequent loop cycles [1][4]. Users should be aware that standard HTTP request actions in automations still carry a risk of blocking the loop if the remote server is unresponsive [1].
Citations:
- 1: [http_request] don't block loop with actions esphome/esphome#7892
- 2: https://esphome.io/api/http__request__arduino_8cpp_source
- 3: http_request component esphome/esphome#719
- 4: https://api-docs-dev.esphome.io/http__request__update_8cpp_source
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the reviewed YAML and any ESPHome source/docs in the repo.
git ls-files | rg '(^|/)Integrations/ESPHome/Core\.yaml$|update(_perform|\.cpp|\.h)|http_request|esp32_ble|ble' || true
# Show the reviewed snippet with line numbers.
sed -n '650,710p' Integrations/ESPHome/Core.yaml
# Search for update.perform implementation or references if present in repo.
rg -n --hidden --no-ignore-vcs 'update\.perform|class .*Update|defer\(|global_ble|disable\(\)|enable\(\)' . || trueRepository: ApolloAutomation/MSR-1
Length of output: 2576
🌐 Web query:
ESPHome http_request update perform source deferred action returns before completion lambda after update.perform
💡 Result:
In ESPHome, the http_request.send action (used by http_request.get, http_request.post, etc.) historically behaves as a blocking operation within the automation chain [1][2][3]. Because it is designed as a synchronous action, the execution of any subsequent lambdas or actions defined in your script will not occur until the HTTP request has fully completed and the response has been processed [1]. In the context of ESPHome automations, actions are generally executed in the order they appear. For an action to be non-blocking and allow the automation to "continue" before completion, it must be explicitly designed as an asynchronous action (where play_next_() is deferred and called later via a timer, loop callback, or event handler) [4][5]. The http_request action, however, does not defer its completion in this manner; it waits for the underlying network library (such as cpp-httplib) to return [1]. If you find that your code is executing unexpectedly or you need to perform actions in parallel, consider the following: 1. Blocking Nature: The http_request action blocks the main ESPHome loop until the request completes [1][3]. If your request takes a long time, it can cause watchdog timeouts or impact other system functions [1]. 2. Script Modes: If you are using scripts, the mode (single, restart, parallel) affects how multiple instances of the same script interact [6]. If a script instance is triggered while another is still blocked by a long-running HTTP request, the script's behavior will depend on its defined mode [6]. 3. Architecture: ESPHome actions are typically synchronous by default unless they specifically implement the asynchronous pattern (registering callbacks to call play_next_()) [4][5]. Since http_request does not currently offload the request to an asynchronous worker that returns control immediately, the "completion lambda" or subsequent actions will always wait for the request to conclude [1]. If you require non-blocking execution, you would typically need to move the request into a custom component that handles the network communication in a separate task or uses an asynchronous networking library that does not block the main loop [1]. Using standard http_request actions will remain synchronous and block the progression of the current script's action list [1].
Citations:
- 1: [http_request] don't block loop with actions esphome/esphome#7892
- 2: Non-blocking http requests esphome/feature-requests#799
- 3: http_request component esphome/esphome#719
- 4: https://developers.esphome.io/blog/2026/03/12/register_action-now-requires-explicit-synchronous-parameter/
- 5: [core] Require explicit synchronous= for register_action esphome/esphome#14606
- 6: Scripts with delay no longer run concurrently in parallel mode from 2025.7.2 esphome/esphome#10264
🌐 Web query:
site:github.com/esphome/esphome "update.perform" deferred play_next_ action http_request_update.cpp
💡 Result:
In the context of ESPHome's source code, specifically within esphome/components/http_request/update/http_request_update.cpp, the use of.defer with actions like play_next_ is a pattern used to ensure thread safety when code runs in a background FreeRTOS task [1][2]. The.defer method schedules a callback to be executed on the ESPHome main loop thread [1][2]. This is critical in http_request_update.cpp because the update check typically runs in a dedicated background task (to prevent watchdog timeouts during network operations) [3][4]. Accessing shared components, state, or triggering automations directly from this background task can lead to data races or memory corruption if those objects are not thread-safe or if they perform non-atomic read-modify-write operations on components [1][2]. By wrapping operations—such as calling play_next_ after an action completes, updating status errors, or triggering events—inside a.defer lambda, the code ensures that these state changes occur exclusively on the main loop [1][2]. This pattern prevents concurrent access to shared resources like the component's state or the global protobuf buffer [1][2]. Similar patterns are seen in core automation components, where play_next_ is often called from deferred callbacks (e.g., via timers or event handlers) to maintain the state machine integrity of the automation [5][6].
Citations:
- 1: [update, http_request_update] Implement update available trigger esphome/esphome#9174
- 2: [http_request] Fix data race on update_info_ strings in update task esphome/esphome#14909
- 3: [http_request] Bugfix: run update function in a task esphome/esphome#8018
- 4: esphome/esphome@5bbf9153c
- 5: [core] Require explicit synchronous= for register_action esphome/esphome#14606
- 6: [core] Replace std::bind with lambda in DelayAction esphome/esphome#14968
Lines 694-698: restore BLE only if you disabled it here. The trailing lambda re-enables BLE unconditionally on a failed update; if BLE was already off, this turns it back on unexpectedly. Capture the original state and only re-enable when you actually disabled it.
🤖 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 665 - 699, The firmware update
flow unconditionally re-enables BLE in the trailing lambda even when it may not
have been disabled by this action. In the update action sequence under the
“Firmware Update” template, capture whether `esp32_ble::global_ble` was active
before disabling it, then only call `enable()` in the fallback path when this
step actually turned BLE off. Keep the state check and restore logic paired with
the existing disable/enable lambdas so the original BLE state is preserved.
Second half of the AIR-1 #107 split: lets users swap between the standard and Bluetooth-proxy images from HA instead of reflashing over USB or editing YAML. - Bluetooth Proxy select (Disabled/Enabled) joins Firmware Channel in composing the OTA manifest URL via apply_ota_source. - On boot each image publishes its real identity into the select (ble_firmware substitution), so a failed or abandoned switch snaps back to the truth. - Same-version variant switches install via the existing Firmware Update force button (merged in #89), which already frees heap by disabling BLE during the download. No workflow changes: build-beta.yml and Pages already publish both variants and their manifests. Version: 26.7.8.3 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Second half of the AIR-1 #107 split: lets users swap between the standard and Bluetooth-proxy images from HA instead of reflashing over USB or editing YAML. - Bluetooth Proxy select (Disabled/Enabled) joins Firmware Channel in composing the OTA manifest URL via apply_ota_source. - On boot each image publishes its real identity into the select (ble_firmware substitution), so a failed or abandoned switch snaps back to the truth. - Same-version variant switches install via the existing Firmware Update force button (merged in #89), which already frees heap by disabling BLE during the download. No workflow changes: build-beta.yml and Pages already publish both variants and their manifests. Version: 26.7.8.5 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Version: 26.7.7.1
What does this implement/fix?
Same Stable/Beta firmware switching as CAST-1 (naming per ApolloAutomation/CAST-1#43):
apply_ota_sourcescript. Stable = GitHub Pages (main branch builds), Beta = rollingbetapre-release assets. Each image tracks its own variant's manifests through a build-timeble_firmwaresubstitution; there is no user-facing variant switching.betabuild the end-user images and publish them to a rollingbetapre-release, with manifests rewritten to absolute URLs and bins prefixed per variant.MSR-1.yamlasfirmware/and moves the Factory image tofirmware-factory/, which only the web installer uses. On their next update, fielded devices leave the Factory image, drop improv BLE, and reclaim flash.All three variants config-validated on ESPHome 2026.6.4. Not yet tested on hardware.
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