From 50d943bbfeecbd3a2114b30c31fd3a0f9d1cf326 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:27:04 -0500 Subject: [PATCH] Add Firmware Channel switching and serve end-user images for OTA 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) sets the OTA manifest URL via a new apply_ota_source script keyed by a per-variant variant_slug substitution ("" / "-b"); Firmware Update button force-installs and prevents deep sleep for the download duration. - The Minimal yamls (the adopted end-user configs) gain the managed update system, and their device identity is aligned with the improv images (apollo-plt-1 / apollo-plt-1b) since they become the images fielded devices update onto. - build.yml now serves the Minimal images at firmware/ and firmware-b/; the improv images move to *-factory/ for the web installer only. - build-beta.yml publishes beta builds of both variants to a rolling beta pre-release. - update component id renamed firmware_update -> update_http_request to match the other Apollo repos. The BLE variant yamls also gain the managed update system (they inherit the Firmware Update button and apply_ota_source from Core.yaml, so they must define update_http_request for CI to pass). 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 | 103 ++++++++++++++++++ .github/workflows/build.yml | 8 +- Integrations/ESPHome/.gitignore | 1 + Integrations/ESPHome/Core.yaml | 90 ++++++++++++++- Integrations/ESPHome/PLT-1.yaml | 4 +- Integrations/ESPHome/PLT-1B.yaml | 7 +- Integrations/ESPHome/PLT-1B_BLE.yaml | 16 +++ Integrations/ESPHome/PLT-1B_Minimal.yaml | 26 ++++- Integrations/ESPHome/PLT-1_BLE.yaml | 13 +++ Integrations/ESPHome/PLT-1_Minimal.yaml | 23 +++- .../ESPHome/beta-channel/PLT-1B_Minimal.yaml | 9 ++ .../ESPHome/beta-channel/PLT-1_Minimal.yaml | 9 ++ static/index.html | 4 +- 13 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/build-beta.yml create mode 100644 Integrations/ESPHome/beta-channel/PLT-1B_Minimal.yaml create mode 100644 Integrations/ESPHome/beta-channel/PLT-1_Minimal.yaml diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..e910585 --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,103 @@ +name: Build and Publish Beta + +# Builds PLT-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: + # Beta serves OTA updates only, so it builds the end-user Minimal + # images, not the first-flash improv images. + - { yaml: Integrations/ESPHome/beta-channel/PLT-1_Minimal.yaml, name: firmware-beta } + - { yaml: Integrations/ESPHome/beta-channel/PLT-1B_Minimal.yaml, name: firmware-b-beta } + 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 PLT-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" + # Manifest names match the on-device variant_slug: "" and "-b". + declare -A DIRS=( [""]=firmware-beta ["-b"]=firmware-b-beta ) + for v in "" "-b"; do + src="fw/${DIRS[$v]}" + man=$(find "$src" -name manifest.json | head -1) + if [ -z "$man" ]; then + echo "::error::manifest.json not found for ${DIRS[$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 redirect. + # The two variants have distinct device names, so bin filenames + # don't collide in the flat release-asset namespace. + 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 "$src" -name '*.bin' -print -exec \ + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; + done + echo "Beta assets published." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6173e3..8c8bf5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,10 +24,16 @@ jobs: pull-requests: write with: device-name: plt-1 + # The Minimal yamls are the end-user images served at firmware/ and + # firmware-b/ (OTA updates and dashboard adoption). The improv images + # move to *-factory/ and are only used for first flashes via the web + # installer. yaml-files: | + Integrations/ESPHome/PLT-1_Minimal.yaml + Integrations/ESPHome/PLT-1B_Minimal.yaml Integrations/ESPHome/PLT-1.yaml Integrations/ESPHome/PLT-1B.yaml - firmware-names: "1:firmware,1B:firmware-b" + firmware-names: "1_Minimal:firmware,1B_Minimal:firmware-b,1:firmware-factory,1B:firmware-b-factory" core-yaml-path: Integrations/ESPHome/Core.yaml esphome-version: stable # Bypass check if manually triggered with bypass option diff --git a/Integrations/ESPHome/.gitignore b/Integrations/ESPHome/.gitignore index d8b4157..78a2a3e 100644 --- a/Integrations/ESPHome/.gitignore +++ b/Integrations/ESPHome/.gitignore @@ -3,3 +3,4 @@ # You can modify this file to suit your needs. /.esphome/ /secrets.yaml +beta-channel/.esphome/ diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 28de2f9..f207833 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,17 @@ substitutions: - version: "26.3.2.1" + version: "26.7.8.1" + # Firmware variant identity: overridden to "-b" by the PLT-1B images so + # each hardware variant tracks its own manifests. + variant_slug: "" + # 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/PLT-1" + beta_manifest_base: "https://github.com/ApolloAutomation/PLT-1/releases/download/beta-fw" esp32: variant: esp32c3 @@ -339,6 +351,52 @@ button: icon: mdi:power-cycle name: "ESP Reboot" + - 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" + # OTA download needs the device awake for its whole duration. + - lambda: |- + id(deep_sleep_1).prevent_deep_sleep(); + # Free heap for the TLS download when BLE (improv) is active. Guarded + # so images without BLE compile 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 + # 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 + - if: + condition: + and: + - switch.is_off: prevent_sleep + - binary_sensor.is_off: ota_mode + then: + - lambda: |- + id(deep_sleep_1).allow_deep_sleep(); + - platform: factory_reset disabled_by_default: True name: "Factory Reset ESP" @@ -385,7 +443,37 @@ text_sensor: update_interval: never entity_category: "diagnostic" +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 + script: + - id: apply_ota_source + # Sets the OTA manifest URL from the Firmware Channel select for this + # hardware variant (${variant_slug}). + # Stable = GitHub Pages, Beta = rolling "beta" release assets. + then: + - lambda: |- + const bool beta = id(firmware_channel).current_option() == "Beta"; + std::string url = beta + ? "${beta_manifest_base}/manifest${variant_slug}.json" + : "${stable_manifest_base}/firmware${variant_slug}/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: statusCheck then: - if: diff --git a/Integrations/ESPHome/PLT-1.yaml b/Integrations/ESPHome/PLT-1.yaml index e70146c..1eac787 100644 --- a/Integrations/ESPHome/PLT-1.yaml +++ b/Integrations/ESPHome/PLT-1.yaml @@ -65,13 +65,13 @@ safe_mode: update: - platform: http_request - id: firmware_update + id: update_http_request name: Firmware Update source: https://apolloautomation.github.io/PLT-1/firmware/manifest.json wifi: on_connect: - - component.update: firmware_update + - component.update: update_http_request ap: ssid: "Apollo PLT1 Hotspot" diff --git a/Integrations/ESPHome/PLT-1B.yaml b/Integrations/ESPHome/PLT-1B.yaml index f6698dc..b932708 100644 --- a/Integrations/ESPHome/PLT-1B.yaml +++ b/Integrations/ESPHome/PLT-1B.yaml @@ -1,3 +1,6 @@ +substitutions: + variant_slug: "-b" + esphome: name: "apollo-plt-1b" friendly_name: Apollo PLT-1B @@ -66,13 +69,13 @@ web_server: update: - platform: http_request - id: firmware_update + id: update_http_request name: Firmware Update source: https://apolloautomation.github.io/PLT-1/firmware-b/manifest.json wifi: on_connect: - - component.update: firmware_update + - component.update: update_http_request ap: ssid: "Apollo PLT1B Hotspot" diff --git a/Integrations/ESPHome/PLT-1B_BLE.yaml b/Integrations/ESPHome/PLT-1B_BLE.yaml index 7c4524a..b8693fd 100644 --- a/Integrations/ESPHome/PLT-1B_BLE.yaml +++ b/Integrations/ESPHome/PLT-1B_BLE.yaml @@ -1,3 +1,6 @@ +substitutions: + variant_slug: "-b" + esphome: name: "apollo-plt-1bble" friendly_name: Apollo PLT-1BBLE @@ -39,6 +42,17 @@ dashboard_import: ota: - platform: esphome id: ota_esphome + - platform: http_request + id: ota_managed + +http_request: + verify_ssl: true + +update: + - platform: http_request + id: update_http_request + name: Firmware Update + source: ${stable_manifest_base}/firmware${variant_slug}/manifest.json safe_mode: @@ -50,6 +64,8 @@ esp32_ble_tracker: active: false wifi: + on_connect: + - component.update: update_http_request ap: ssid: "Apollo PLT1B Hotspot" diff --git a/Integrations/ESPHome/PLT-1B_Minimal.yaml b/Integrations/ESPHome/PLT-1B_Minimal.yaml index bad38b3..372f192 100644 --- a/Integrations/ESPHome/PLT-1B_Minimal.yaml +++ b/Integrations/ESPHome/PLT-1B_Minimal.yaml @@ -1,10 +1,13 @@ +substitutions: + variant_slug: "-b" + esphome: - name: "apollo-plt-1b-minimal" - friendly_name: Apollo PLT-1B Minimal - comment: Apollo PLT-1B Minimal + name: "apollo-plt-1b" + friendly_name: Apollo PLT-1B + comment: Apollo PLT-1B name_add_mac_suffix: true project: - name: "ApolloAutomation.PLT-1B_Minimal" + name: "ApolloAutomation.PLT-1B" version: "${version}" min_version: 2025.6.0 @@ -49,12 +52,27 @@ improv_serial: ota: - platform: esphome id: ota_esphome + - platform: http_request + id: ota_managed + +http_request: + verify_ssl: true + +safe_mode: + +update: + - platform: http_request + id: update_http_request + name: Firmware Update + source: https://apolloautomation.github.io/PLT-1/firmware-b/manifest.json web_server: port: 80 version: 3 wifi: + on_connect: + - component.update: update_http_request ap: ssid: "Apollo PLT1B Hotspot" diff --git a/Integrations/ESPHome/PLT-1_BLE.yaml b/Integrations/ESPHome/PLT-1_BLE.yaml index 62c32fb..e1c22f6 100644 --- a/Integrations/ESPHome/PLT-1_BLE.yaml +++ b/Integrations/ESPHome/PLT-1_BLE.yaml @@ -38,6 +38,17 @@ dashboard_import: ota: - platform: esphome id: ota_esphome + - platform: http_request + id: ota_managed + +http_request: + verify_ssl: true + +update: + - platform: http_request + id: update_http_request + name: Firmware Update + source: ${stable_manifest_base}/firmware${variant_slug}/manifest.json safe_mode: @@ -50,6 +61,8 @@ esp32_ble_tracker: wifi: + on_connect: + - component.update: update_http_request ap: ssid: "Apollo PLT1 Hotspot" diff --git a/Integrations/ESPHome/PLT-1_Minimal.yaml b/Integrations/ESPHome/PLT-1_Minimal.yaml index 9c7d70a..2e74e88 100644 --- a/Integrations/ESPHome/PLT-1_Minimal.yaml +++ b/Integrations/ESPHome/PLT-1_Minimal.yaml @@ -1,10 +1,10 @@ esphome: - name: "apollo-plt-1-minimal" - friendly_name: Apollo PLT-1 Minimal - comment: Apollo PLT-1 Minimal + name: "apollo-plt-1" + friendly_name: Apollo PLT-1 + comment: Apollo PLT-1 name_add_mac_suffix: true project: - name: "ApolloAutomation.PLT-1_Minimal" + name: "ApolloAutomation.PLT-1" version: "${version}" min_version: 2025.6.0 @@ -48,12 +48,27 @@ improv_serial: ota: - platform: esphome id: ota_esphome + - platform: http_request + id: ota_managed + +http_request: + verify_ssl: true + +safe_mode: + +update: + - platform: http_request + id: update_http_request + name: Firmware Update + source: https://apolloautomation.github.io/PLT-1/firmware/manifest.json web_server: port: 80 version: 3 wifi: + on_connect: + - component.update: update_http_request ap: ssid: "Apollo PLT1 Hotspot" diff --git a/Integrations/ESPHome/beta-channel/PLT-1B_Minimal.yaml b/Integrations/ESPHome/beta-channel/PLT-1B_Minimal.yaml new file mode 100644 index 0000000..be924c8 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/PLT-1B_Minimal.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of PLT-1B_Minimal.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 PLT-1B_Minimal.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../PLT-1B_Minimal.yaml diff --git a/Integrations/ESPHome/beta-channel/PLT-1_Minimal.yaml b/Integrations/ESPHome/beta-channel/PLT-1_Minimal.yaml new file mode 100644 index 0000000..1387589 --- /dev/null +++ b/Integrations/ESPHome/beta-channel/PLT-1_Minimal.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of PLT-1_Minimal.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 PLT-1_Minimal.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../PLT-1_Minimal.yaml diff --git a/static/index.html b/static/index.html index 81d3f8f..153f47f 100644 --- a/static/index.html +++ b/static/index.html @@ -84,11 +84,11 @@