From 6203ffaa11175c0397ddd3efd538a0414559692a 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] Add Firmware Channel select for Stable/Beta OTA switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same feature as CAST-1 and AIR-1 (ApolloAutomation/AIR-1#107): - Firmware Channel select (Stable/Beta) in Core.yaml combines with the existing per-variant Firmware Type select via a shared apply_ota_source script; the inline URL if/else in the W/ETH yamls moves there. - build-beta.yml publishes beta builds of both variants to a rolling beta pre-release with manifests rewritten to absolute URLs. Beta-channel builds compile thin beta-channel/ wrapper yamls so the Firmware Channel select defaults to Beta on firmware obtained from the beta channel (fresh flashes only; stored choices still win). Version: 26.7.8.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/build-beta.yml | 96 +++++++++++++++++++ Integrations/ESPHome/Core.yaml | 48 +++++++++- Integrations/ESPHome/R_PRO-1_ETH.yaml | 14 +-- Integrations/ESPHome/R_PRO-1_W.yaml | 14 +-- .../ESPHome/beta-channel/R_PRO-1_ETH.yaml | 9 ++ .../ESPHome/beta-channel/R_PRO-1_W.yaml | 9 ++ 6 files changed, 167 insertions(+), 23 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..d1eac51 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,14 @@ substitutions: - version: "26.6.10.1" + version: "26.7.8.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" 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" esp32: variant: esp32s3 @@ -665,6 +674,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' @@ -814,6 +838,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" 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..4daf816 100644 --- a/Integrations/ESPHome/R_PRO-1_ETH.yaml +++ b/Integrations/ESPHome/R_PRO-1_ETH.yaml @@ -80,17 +80,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..0fe7048 100644 --- a/Integrations/ESPHome/R_PRO-1_W.yaml +++ b/Integrations/ESPHome/R_PRO-1_W.yaml @@ -83,17 +83,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