From 09c09459f75e4bab8413713eab9b9a12d298257c Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:20:50 -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 port 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 every image; a "Bluetooth Proxy" switch (default off, persisted, re-applied at boot) starts/stops scanning at runtime. Always-on C6/8MB device - a clean proxy candidate; nothing changes until a user turns the switch on. - Firmware Channel select (Stable/Beta) + apply_ota_source doing a direct set_source_url swap from ota_stable_manifest/ota_beta_manifest subs; the channel is re-applied at boot (on_boot -100) - Firmware Update button simplified: no pre-OTA BLE disable (ESPHome's OTA quiesces BLE itself) - http_request consolidated into Core.yaml with the proven buffer sizes (rx 5120 / tx 2048 for GitHub release redirects) - PUMP-1_Minimal on_boot converted to list form so Core's boot logic (pump safety init, refill-mode application) still runs on the image that now serves OTA - PUMP-1_Minimal gains the managed-update machinery and takes over Pages firmware/ for OTA/adoption; the improv image moves to firmware-factory/ (installer page repointed) - beta-channel/ wrapper defaults the select to Beta; build-beta.yml publishes the manifest (absolute URLs) to the rolling beta-fw pre-release - min_version 2025.11.0 (update-system floor); version 26.7.12.1 Supersedes #45. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/build-beta.yml | 91 ++++++++++++++++ .github/workflows/build.yml | 6 +- Integrations/ESPHome/Core.yaml | 102 +++++++++++++++++- Integrations/ESPHome/PUMP-1.yaml | 9 +- Integrations/ESPHome/PUMP-1_Minimal.yaml | 26 +++-- .../ESPHome/beta-channel/PUMP-1_Minimal.yaml | 9 ++ static/index.html | 2 +- 7 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/build-beta.yml create mode 100644 Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml new file mode 100644 index 0000000..dd1f78a --- /dev/null +++ b/.github/workflows/build-beta.yml @@ -0,0 +1,91 @@ +name: Build and Publish Beta + +# Builds PUMP-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 firmware + needs: version + # Beta serves OTA updates only, so it builds the end-user image + # (PUMP-1_Minimal.yaml), not the first-flash improv image. + uses: esphome/workflows/.github/workflows/build.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1 + with: + files: Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml + esphome-version: stable + combined-name: firmware-beta + 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 PUMP-1 beta firmware. Auto-updated on every push to the beta branch." + + - name: Rewrite manifest to absolute URLs and upload assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE="https://github.com/${{ github.repository }}/releases/download/beta-fw" + man=$(find fw/firmware-beta -name manifest.json | head -1) + if [ -z "$man" ]; then + echo "::error::manifest.json not found" + 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. + jq --arg base "$BASE" ' + .builds[0].ota.path = ($base + "/" + (.builds[0].ota.path | sub(".*/"; ""))) + | .builds[0].parts |= map(.path = ($base + "/" + (.path | sub(".*/"; "")))) + ' "$man" > manifest.json + cat manifest.json + gh release upload beta-fw manifest.json -R "${{ github.repository }}" --clobber + find fw/firmware-beta -name '*.bin' -print -exec \ + gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; + echo "Beta assets published." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b8377e..b34e36b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,9 +24,13 @@ jobs: pull-requests: write with: device-name: pump-1 + # PUMP-1_Minimal.yaml is the end-user image served at firmware/ (OTA + # updates and dashboard adoption). PUMP-1.yaml (improv + BLE) is only + # used for first flashes via the web installer. yaml-files: | + Integrations/ESPHome/PUMP-1_Minimal.yaml Integrations/ESPHome/PUMP-1.yaml - firmware-names: "1:firmware" + firmware-names: "1_Minimal:firmware,1:firmware-factory" core-yaml-path: Integrations/ESPHome/Core.yaml esphome-version: stable # Bypass check if manually triggered with bypass option diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index a5e0790..150f811 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,13 +1,32 @@ substitutions: name: apollo-pump-1 - version: "26.3.2.1" + version: "26.7.12.1" device_description: ${name} made by Apollo Automation - version ${version}. + # 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/PUMP-1" + beta_manifest_base: "https://github.com/ApolloAutomation/PUMP-1/releases/download/beta-fw" + # OTA manifest URLs picked by apply_ota_source. + ota_stable_manifest: "${stable_manifest_base}/firmware/manifest.json" + ota_beta_manifest: "${beta_manifest_base}/manifest.json" esp32: variant: esp32c6 flash_size: 8MB framework: type: esp-idf + +esp32_ble_tracker: + id: ble_tracker + scan_parameters: + continuous: true + +bluetooth_proxy: esphome: on_boot: @@ -30,6 +49,22 @@ esphome: - switch.turn_off: stop_pump_when_full - switch.turn_on: stop_pump_when_dry - switch.turn_off: auto_refill + # Point the update entity at the selected channel's 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: - priority: -100 then: - delay: 1000ms @@ -83,6 +118,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 + # Buzzer output: - platform: ledc @@ -96,6 +142,21 @@ 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 update for the selected 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); + - platform: factory_reset disabled_by_default: True name: "Factory Reset ESP" @@ -220,6 +281,19 @@ switch: id(pump_start_time) = 0; id(safety_alert_active) = 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: + binary_sensor: - platform: status name: Online @@ -384,7 +458,33 @@ 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 (Stable/Beta). + then: + - lambda: |- + const bool beta = id(firmware_channel).current_option() == "Beta"; + std::string url = beta ? "${ota_beta_manifest}" : "${ota_stable_manifest}"; + 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: pumpUntilFull then: - switch.turn_on: stop_pump_when_full diff --git a/Integrations/ESPHome/PUMP-1.yaml b/Integrations/ESPHome/PUMP-1.yaml index d82dd26..c4f53a2 100644 --- a/Integrations/ESPHome/PUMP-1.yaml +++ b/Integrations/ESPHome/PUMP-1.yaml @@ -21,7 +21,7 @@ esphome: name: "ApolloAutomation.PUMP-1" version: "${version}" - min_version: 2023.11.1 + min_version: 2025.11.0 dashboard_import: package_import_url: github://ApolloAutomation/PUMP-1/Integrations/ESPHome/PUMP-1_Minimal.yaml @@ -38,20 +38,17 @@ ota: - platform: http_request id: ota_managed -http_request: - verify_ssl: true - safe_mode: update: - platform: http_request - id: firmware_update + id: update_http_request name: Firmware Update source: https://apolloautomation.github.io/PUMP-1/firmware/manifest.json wifi: on_connect: - - component.update: firmware_update + - component.update: update_http_request ap: ssid: "Apollo PUMP-1 Hotspot" diff --git a/Integrations/ESPHome/PUMP-1_Minimal.yaml b/Integrations/ESPHome/PUMP-1_Minimal.yaml index 85c674b..b74619b 100644 --- a/Integrations/ESPHome/PUMP-1_Minimal.yaml +++ b/Integrations/ESPHome/PUMP-1_Minimal.yaml @@ -7,13 +7,15 @@ esphome: name: "ApolloAutomation.PUMP-1" version: "${version}" - min_version: 2023.11.1 + min_version: 2025.11.0 + # List form so package merging concatenates with Core.yaml's on_boot + # entries (mapping form would replace Core's whole list instead). on_boot: - priority: 500 - then: - - text_sensor.template.publish: - id: apollo_firmware_version - state: "${version}" + - priority: 500 + then: + - text_sensor.template.publish: + id: apollo_firmware_version + state: "${version}" dashboard_import: package_import_url: github://ApolloAutomation/PUMP-1/Integrations/ESPHome/PUMP-1_Minimal.yaml @@ -22,8 +24,20 @@ dashboard_import: ota: - platform: esphome id: ota_esphome + - platform: http_request + id: ota_managed + +safe_mode: + +update: + - platform: http_request + id: update_http_request + name: Firmware Update + source: https://apolloautomation.github.io/PUMP-1/firmware/manifest.json wifi: + on_connect: + - component.update: update_http_request ap: ssid: "Apollo PUMP-1 Hotspot" diff --git a/Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml b/Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml new file mode 100644 index 0000000..abc4f8d --- /dev/null +++ b/Integrations/ESPHome/beta-channel/PUMP-1_Minimal.yaml @@ -0,0 +1,9 @@ +# Beta-channel build of PUMP-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 PUMP-1_Minimal.yaml directly. +substitutions: + firmware_channel_default: "Beta" + +packages: + base: !include ../PUMP-1_Minimal.yaml diff --git a/static/index.html b/static/index.html index 4f3552d..5adcb58 100644 --- a/static/index.html +++ b/static/index.html @@ -82,7 +82,7 @@

Apollo PUMP-1 Installer

- +

From 903cdd818f86af4d7d6dc6da59b58d315b1beb3d Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:48:46 -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 150f811..ee04968 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,6 +1,6 @@ substitutions: name: apollo-pump-1 - version: "26.7.12.1" + version: "26.7.14.1" device_description: ${name} made by Apollo Automation - version ${version}. # 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 @@ -78,6 +78,7 @@ esphome: - lambda: "id(testScript).execute();" api: + encryption: actions: - action: play_buzzer variables: From d8512edc9d95f546e52bd3d37cf645198de378e6 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:59:25 -0500 Subject: [PATCH 3/3] Repoint beta-fw tag after publish and pin Workflows build.yml to SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish-beta job uploaded assets to the rolling beta-fw pre-release but never moved the tag, so gh release create left the tag on the default branch HEAD and the release's source commit drifted away from the firmware actually published. Add a final step that force-updates the beta-fw tag ref to the built commit, mirroring AIR-1 #117. Also pin the ApolloAutomation/Workflows build.yml reusable-workflow ref from the mutable @main to commit 430d90dc (main 2026-07-23), matching how third-party actions are already pinned, per AIR-1 #118. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .github/workflows/build-beta.yml | 11 +++++++++++ .github/workflows/build.yml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-beta.yml b/.github/workflows/build-beta.yml index dd1f78a..dd19a32 100644 --- a/.github/workflows/build-beta.yml +++ b/.github/workflows/build-beta.yml @@ -89,3 +89,14 @@ jobs: find fw/firmware-beta -name '*.bin' -print -exec \ gh release upload beta-fw {} -R "${{ github.repository }}" --clobber \; 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 }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b34e36b..3da6965 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ on: jobs: build-and-publish: - uses: ApolloAutomation/Workflows/.github/workflows/build.yml@main + uses: ApolloAutomation/Workflows/.github/workflows/build.yml@430d90dc695c6f7d1075c4e4a0df4b13a6496252 # main 2026-07-23 permissions: contents: write pages: write