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