From 6a7f488f7eb885871f7ef74534edec14e7b1b094 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:14:13 -0500 Subject: [PATCH 1/3] Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild of the earlier firmware-channel branch 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 both images; a "Bluetooth Proxy" switch (default off, persisted, re-applied at boot) starts/stops scanning at runtime. Always-on S3/8MB device - a clean proxy candidate; nothing changes until a user turns the switch on. - Firmware Channel select (Stable/Beta) crossed with the existing Firmware Type select (WiFi/Ethernet) in apply_ota_source - a direct set_source_url swap over the four manifests, re-applied at boot (on_boot -100). No manifest-matching guard. - Firmware Update button upgraded from bare perform(true) to the standard flow (apply_ota_source + fetch window before perform) - http_request consolidated into Core.yaml with the proven buffer sizes (rx 5120 / tx 2048 for GitHub release redirects) - beta-channel/ wrappers default the select to Beta; build-beta.yml publishes manifest-e.json + manifest-w.json (absolute URLs) to the rolling beta-fw pre-release - version 26.7.12.1 Supersedes #65. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/build-beta.yml | 96 +++++++++++++++ Integrations/ESPHome/Core.yaml | 112 +++++++++++++++++- Integrations/ESPHome/R_PRO-1_ETH.yaml | 17 +-- Integrations/ESPHome/R_PRO-1_W.yaml | 17 +-- .../ESPHome/beta-channel/R_PRO-1_ETH.yaml | 9 ++ .../ESPHome/beta-channel/R_PRO-1_W.yaml | 9 ++ 6 files changed, 228 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/build-beta.yml create mode 100644 Integrations/ESPHome/beta-channel/R_PRO-1_ETH.yaml create mode 100644 Integrations/ESPHome/beta-channel/R_PRO-1_W.yaml diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..242387e --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,96 @@ +name: Build and Publish Beta + +# Builds R_PRO-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: + - { yaml: Integrations/ESPHome/beta-channel/R_PRO-1_W.yaml, name: firmware-w } + - { yaml: Integrations/ESPHome/beta-channel/R_PRO-1_ETH.yaml, name: firmware-e } + 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 R_PRO-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" + for v in w e; do + man=$(find "fw/firmware-$v" -name manifest.json | head -1) + if [ -z "$man" ]; then + echo "::error::manifest.json not found for firmware-$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 redirected URL. + 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 "fw/firmware-$v" -name '*.bin' -print -exec \ + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; + done + echo "Beta assets published." diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index aa2512b..ee298cd 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,35 @@ substitutions: - version: "26.6.10.1" + version: "26.7.12.1" + # 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/R_PRO-1" + beta_manifest_base: "https://github.com/ApolloAutomation/R_PRO-1/releases/download/beta-fw" + +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: + # Point the update entity at the selected type/channel 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: esp32: variant: esp32s3 @@ -7,6 +37,13 @@ esp32: framework: type: esp-idf +esp32_ble_tracker: + id: ble_tracker + scan_parameters: + continuous: true + +bluetooth_proxy: + api: actions: - action: play_buzzer @@ -58,6 +95,17 @@ web_server: port: 80 version: 3 +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: GPIO41 scl: GPIO40 @@ -129,11 +177,17 @@ button: - platform: template name: "Firmware Update" id: update_firmware + icon: mdi:cloud-download entity_category: config on_press: - then: - - lambda: |- - id(update_http_request).perform(true); + - logger.log: "Applying firmware update for the selected type and channel" + - 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); number: - platform: ld2450 @@ -665,6 +719,21 @@ sensor: id: ld2412_g13_still_energy 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: ld2412 out_pin_level: name: 'LD2412 Hardware output pin level' @@ -748,6 +817,19 @@ switch: id: setCo2AutoCalibration enable: false + - 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: ld2450 ld2450_id: ld2450_radar @@ -814,6 +896,28 @@ light: max_brightness: 100% script: + - id: apply_ota_source + # Sets the OTA manifest URL from the two selectors: Firmware Type + # (WiFi/Ethernet) x Firmware Channel (Stable/Beta). + # Stable = GitHub Pages, Beta = rolling "beta-fw" release assets. + then: + - lambda: |- + const bool eth = id(firmware_selector).current_option() == "Ethernet"; + const bool beta = id(firmware_channel).current_option() == "Beta"; + std::string url; + if (beta) { + url = eth + ? "${beta_manifest_base}/manifest-e.json" + : "${beta_manifest_base}/manifest-w.json"; + } else { + url = eth + ? "${stable_manifest_base}/firmware-e/manifest.json" + : "${stable_manifest_base}/firmware-w/manifest.json"; + } + 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: setCo2AutoCalibration mode: restart parameters: diff --git a/Integrations/ESPHome/R_PRO-1_ETH.yaml b/Integrations/ESPHome/R_PRO-1_ETH.yaml index 16093da..ef1975f 100644 --- a/Integrations/ESPHome/R_PRO-1_ETH.yaml +++ b/Integrations/ESPHome/R_PRO-1_ETH.yaml @@ -44,9 +44,6 @@ ota: - platform: http_request id: ota_managed -http_request: - verify_ssl: true - safe_mode: update: @@ -80,17 +77,9 @@ select: initial_option: "Ethernet" on_value: then: - - if: - condition: - lambda: 'return id(firmware_selector).current_option() == "Ethernet";' - then: - - logger.log: "OTA updates set to use ethernet firmware" - - lambda: id(update_http_request).set_source_url("https://apolloautomation.github.io/R_PRO-1/firmware-e/manifest.json"); - - component.update: update_http_request - else: - - logger.log: "OTA updates set to use wifi firmware" - - lambda: id(update_http_request).set_source_url("https://apolloautomation.github.io/R_PRO-1/firmware-w/manifest.json"); - - component.update: update_http_request + # URL logic lives in Core.yaml's apply_ota_source so it can combine + # Firmware Type with the Firmware Channel (Stable/Beta) select. + - script.execute: apply_ota_source text_sensor: - platform: ethernet_info diff --git a/Integrations/ESPHome/R_PRO-1_W.yaml b/Integrations/ESPHome/R_PRO-1_W.yaml index fb6c89f..b5b56c0 100644 --- a/Integrations/ESPHome/R_PRO-1_W.yaml +++ b/Integrations/ESPHome/R_PRO-1_W.yaml @@ -38,9 +38,6 @@ ota: - platform: http_request id: ota_managed -http_request: - verify_ssl: true - safe_mode: improv_serial: @@ -83,17 +80,9 @@ select: initial_option: "WiFi" on_value: then: - - if: - condition: - lambda: 'return id(firmware_selector).current_option() == "Ethernet";' - then: - - logger.log: "OTA updates set to use ethernet firmware" - - lambda: id(update_http_request).set_source_url("https://apolloautomation.github.io/R_PRO-1/firmware-e/manifest.json"); - - component.update: update_http_request - else: - - logger.log: "OTA updates set to use wifi firmware" - - lambda: id(update_http_request).set_source_url("https://apolloautomation.github.io/R_PRO-1/firmware-w/manifest.json"); - - component.update: update_http_request + # URL logic lives in Core.yaml's apply_ota_source so it can combine + # Firmware Type with the Firmware Channel (Stable/Beta) select. + - script.execute: apply_ota_source text_sensor: - platform: wifi_info diff --git a/Integrations/ESPHome/beta-channel/R_PRO-1_ETH.yaml b/Integrations/ESPHome/beta-channel/R_PRO-1_ETH.yaml new file mode 100644 index 0000000..c8f2b22 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/R_PRO-1_ETH.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of R_PRO-1_ETH.yaml: the identical image except the Firmware +# Channel select defaults to "Beta" on first boot, so firmware obtained from +# the beta channel keeps tracking it. Built by build-beta.yml only; the +# stable (GitHub Pages) builds use R_PRO-1_ETH.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../R_PRO-1_ETH.yaml diff --git a/Integrations/ESPHome/beta-channel/R_PRO-1_W.yaml b/Integrations/ESPHome/beta-channel/R_PRO-1_W.yaml new file mode 100644 index 0000000..29e40a0 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/R_PRO-1_W.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of R_PRO-1_W.yaml: the identical image except the Firmware +# Channel select defaults to "Beta" on first boot, so firmware obtained from +# the beta channel keeps tracking it. Built by build-beta.yml only; the +# stable (GitHub Pages) builds use R_PRO-1_W.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../R_PRO-1_W.yaml From f9f8af5ff5245e5b5e9d67dfbdc8851fe0a03b53 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:48:37 -0500 Subject: [PATCH 2/3] Enable API encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Integrations/ESPHome/Core.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index ee298cd..55f5554 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.7.12.1" + version: "26.7.14.1" # 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 @@ -45,6 +45,7 @@ esp32_ble_tracker: bluetooth_proxy: api: + encryption: actions: - action: play_buzzer variables: From c9bb4cec73a7190bb8e58d5f10feec90bfeb3267 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:00:21 -0500 Subject: [PATCH 3/3] Repoint beta-fw tag at built commit after publishing assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh release create tags the rolling beta-fw pre-release at the default branch HEAD, and gh release upload never moves the tag. Without an explicit repoint the release's source commit drifts away from the firmware assets actually published on each beta push, so the tag no longer identifies the code that produced the binaries. Add a final step to the publish-beta job that force-updates the beta-fw tag ref to github.sha once assets are uploaded, matching the AIR-1 26.7.23.1 release fixes (AIR-1 #117, #118). Note: build-beta.yml uses the esphome/workflows reusable workflow, which is already pinned to a commit SHA, so no ApolloAutomation Workflows @main refs exist in this file to pin here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/build-beta.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index 242387e..b903c26 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -94,3 +94,14 @@ jobs: 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 }}"