Skip to content

Add Firmware Channel switching from HA#89

Merged
TrevorSchirmer merged 1 commit into
betafrom
firmware-channel
Jul 8, 2026
Merged

Add Firmware Channel switching from HA#89
TrevorSchirmer merged 1 commit into
betafrom
firmware-channel

Conversation

@bharvey88

@bharvey88 bharvey88 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Version: 26.7.7.1

What does this implement/fix?

Same Stable/Beta firmware switching as CAST-1 (naming per ApolloAutomation/CAST-1#43):

  • Firmware Channel select (Stable/Beta) sets the OTA manifest URL via a new apply_ota_source script. Stable = GitHub Pages (main branch builds), Beta = rolling beta pre-release assets. Each image tracks its own variant's manifests through a build-time ble_firmware substitution; there is no user-facing variant switching.
  • Firmware Update button force-installs the selected channel's firmware. It re-applies the manifest URL first (so a just-switched channel is fetched before forcing) and temporarily disables BLE during the download to free heap for TLS (no-op on non-BLE images).
  • build-beta.yml (new): pushes to beta build the end-user images and publish them to a rolling beta pre-release, with manifests rewritten to absolute URLs and bins prefixed per variant.
  • Updates now serve the end-user image: CI builds MSR-1.yaml as firmware/ and moves the Factory image to firmware-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

  • Bugfix (fixed change that fixes an issue)
  • New feature (thanks!)
  • Breaking change (repair/feature that breaks existing functionality)
  • Dependency Update - Does not publish
  • Other - Does not publish
  • Website of github readme file update - Does not publish
  • Github workflows - Does not publish

Checklist / Checklijst:

  • The code change has been tested and works locally
  • The code change has not yet been tested

If user-visible functionality or configuration variables are added/modified:

  • Added/updated documentation for the web page

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a beta firmware channel with a dedicated release process and downloadable beta builds.
    • Added a firmware update control and channel selector to let users switch between Stable and Beta releases.
    • Updated Bluetooth-enabled firmware to use the beta update path.
  • Bug Fixes

    • Improved firmware download links and asset naming so the correct files are served for each firmware variant.
    • Updated the default install flow to point to the proper factory firmware metadata.

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)
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new build-beta.yml GitHub Actions workflow to build and publish beta ESPHome firmware releases, updates the stable build.yml firmware naming, bumps the Core.yaml version, adds OTA update/channel-selection logic with a ble_firmware substitution, and updates the static page's manifest path.

Changes

Beta Build Pipeline and OTA Channel Support

Layer / File(s) Summary
Stable build firmware naming
.github/workflows/build.yml
Adds MSR-1.yaml to the yaml-files list and remaps firmware-names so Factory outputs firmware-factory and the new variant outputs firmware.
Static install page manifest path
static/index.html
Updates the install button's manifest attribute to ./firmware-factory/manifest.json.
OTA substitutions and BLE flag
Integrations/ESPHome/Core.yaml, Integrations/ESPHome/MSR-1_BLE.yaml
Bumps firmware version, adds ble_firmware substitution (false by default, true in BLE variant), and adds stable_manifest_base/beta_manifest_base URLs.
Firmware update button and channel script
Integrations/ESPHome/Core.yaml
Adds a Firmware Update button that toggles BLE and triggers update.perform, plus a Firmware Channel select and apply_ota_source script that computes and applies the OTA manifest URL.
Beta build and publish workflow
.github/workflows/build-beta.yml
New workflow that extracts version, builds two ESPHome targets via shared workflow, rewrites manifest URLs, renames binaries, and publishes them to a beta prerelease.

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
Loading
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
Loading

Possibly related PRs

  • ApolloAutomation/MSR-1#79: Both PRs modify the firmware naming wiring in .github/workflows/build.yml (the firmware-names input/mapping for Factory/variant handling).
  • ApolloAutomation/MSR-1#85: Both PRs bump substitutions.version in Integrations/ESPHome/Core.yaml.

Poem

A hop, a build, a beta glow,
New firmware channels start to flow. 🐇
BLE toggles off then back,
Manifests renamed and packed,
This rabbit thumps approval — go!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main user-facing change: adding Home Assistant firmware channel switching.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch firmware-channel

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e0e7c29 and 7616da5.

📒 Files selected for processing (5)
  • .github/workflows/build-beta.yml
  • .github/workflows/build.yml
  • Integrations/ESPHome/Core.yaml
  • Integrations/ESPHome/MSR-1_BLE.yaml
  • static/index.html

Comment on lines +665 to +699
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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:


🏁 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\(\)' . || true

Repository: 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:


🌐 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:


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.

@TrevorSchirmer
TrevorSchirmer merged commit 73fd740 into beta Jul 8, 2026
10 checks passed
@TrevorSchirmer
TrevorSchirmer deleted the firmware-channel branch July 8, 2026 15:06
bharvey88 added a commit that referenced this pull request Jul 8, 2026
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)
bharvey88 added a commit that referenced this pull request Jul 8, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants