Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA#46
Unified firmware: runtime Bluetooth Proxy switch + Stable/Beta channel OTA#46bharvey88 wants to merge 3 commits into
Conversation
WalkthroughThe PR adds channel-based OTA configuration, BLE proxy controls, beta firmware build and release automation, dual firmware outputs, and a factory-manifest web installer target. ChangesFirmware delivery
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Device
participant FirmwareChannel
participant ApplyOtaSource
participant UpdateHttpRequest
participant ManifestEndpoint
Device->>FirmwareChannel: Select Stable or Beta
FirmwareChannel->>ApplyOtaSource: Apply selected channel
ApplyOtaSource->>UpdateHttpRequest: Set manifest source URL
Device->>UpdateHttpRequest: Perform firmware update
UpdateHttpRequest->>ManifestEndpoint: Fetch selected manifest
ManifestEndpoint-->>UpdateHttpRequest: Return firmware asset URLs
UpdateHttpRequest-->>Device: Download selected firmware
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…l OTA 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)
15669c5 to
09c0945
Compare
|
Updated: per fleet policy every product now gets the runtime Bluetooth Proxy switch, so this PR adds bluetooth_proxy + esp32_ble_tracker to Core with the standard switch (default off). Branch force-pushed; body updated. |
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)
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)
|
Updated this branch (d8512ed) with two fixes from the AIR-1 26.7.23.1 release:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/build-beta.yml:
- Around line 48-102: Add workflow-level concurrency to build-beta.yml using a
stable beta publication group such as `${{ github.workflow }}-${{ github.ref
}}`. Configure the guard to serialize or cancel overlapping runs, ensuring only
the latest beta publication can upload assets and update the beta-fw tag; place
it at the workflow level rather than inside the publish-beta job.
In `@Integrations/ESPHome/Core.yaml`:
- Line 81: Update the ESPHome API encryption configuration under the encryption
block to provide encryption.key with a securely provisioned, valid 32-byte
base64-encoded value. Keep the existing encryption configuration structure and
ensure the key is not empty.
- Around line 146-160: Update the Firmware Update flow around update_firmware
and the firmware_channel on_value handler so manifest retrieval is synchronized
through a completion callback or explicit state machine before invoking
update_http_request.perform(true). Remove reliance on the fixed five-second
delay, ensure installation uses metadata fetched for the newly selected channel,
and apply the same synchronization behavior to both update triggers.
- Around line 24-27: Set esp32_ble_tracker.scan_parameters.continuous to false
so BLE scanning does not begin automatically at startup while the proxy switch
is off. Preserve the existing start_scan actions that explicitly enable
continuous scanning when needed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: babb8192-42a2-402a-a965-0ee73d7d0a44
📒 Files selected for processing (7)
.github/workflows/build-beta.yml.github/workflows/build.ymlIntegrations/ESPHome/Core.yamlIntegrations/ESPHome/PUMP-1.yamlIntegrations/ESPHome/PUMP-1_Minimal.yamlIntegrations/ESPHome/beta-channel/PUMP-1_Minimal.yamlstatic/index.html
| 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." | ||
|
|
||
| - 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 }}" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## workflow locations"
git ls-files | grep -E '(^|/)\.github/workflows/' || true
echo
echo "## build-beta.yml outline/text relevant section"
if [ -f .github/workflows/build-beta.yml ]; then
nl -ba .github/workflows/build-beta.yml | sed -n '1,180p'
else
echo ".github/workflows/build-beta.yml not found"
fi
echo
echo "## all workflow concurrency/release refs mentioning beta"
rg -n "concurrency:|cancel-in-progress:|github\.run_id|beta-fw|action-idempotency-token|GITHUB_SHA|release" .github/workflows || true
echo
echo "## git diff stat/name"
git diff --stat
git diff --name-onlyRepository: ApolloAutomation/PUMP-1
Length of output: 455
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## workflow locations"
git ls-files | grep -E '(^|/)\.github/workflows/' || true
echo
echo "## build-beta.yml relevant section"
if [ -f .github/workflows/build-beta.yml ]; then
awk '{printf "%5d: %s\n", NR, $0}' .github/workflows/build-beta.yml | sed -n '1,180p'
else
echo ".github/workflows/build-beta.yml not found"
fi
echo
echo "## all workflow concurrency/release refs mentioning beta"
rg -n "concurrency:|cancel-in-progress:|github\.run_id|beta-fw|action-idempotency-token|GITHUB_SHA|release" .github/workflows || true
echo
echo "## git diff stat/name"
git diff --stat || true
git diff --name-only || trueRepository: ApolloAutomation/PUMP-1
Length of output: 6921
🌐 Web query:
GitHub Actions workflow concurrency workflow concurrency group cancel-in-progress same workflow branch multiple runs
💡 Result:
To manage concurrency in GitHub Actions workflows effectively, you can use the concurrency key to group runs and control whether in-progress runs are cancelled when a new run is triggered [1][2][3]. Key Concepts: Group: This defines a unique label for a concurrency group. All workflow runs that share the same group name are subjected to the same concurrency limits [1][3]. To restrict cancellation to the same workflow and the same branch (or pull request), a common and recommended practice is to use an expression incorporating both the workflow name and the ref [4][5]: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: This boolean (or expression that evaluates to one) determines the behavior when a new run enters a group that already has an active run [1][6]. - When set to false (default), new runs are queued in a pending state until the in-progress run finishes [7][5][3]. - When set to true, any currently running or pending runs within that same group are cancelled immediately, allowing the new run to proceed [1][4][3]. Conditional Cancellation: You can use expressions to dynamically set cancel-in-progress [1][8]. This is highly useful for preventing unwanted cancellations on critical branches (like main or release branches) while still automatically cancelling outdated runs on feature branches or pull requests [7][4][8]: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref!= 'refs/heads/main' }} Important Considerations: - Avoid Unique Keys: Never include values that are unique to every run, such as github.sha or github.run_id, in your group key. Doing so will create a unique group for every run, rendering the concurrency and cancellation settings ineffective [4]. - Default Behavior: Even with cancel-in-progress set to false, GitHub Actions ensures that at most one run is active and one run is pending per group; additional pending runs will cancel previous pending runs [9][6]. - Scope: If multiple workflows in your repository use the same group name, they will all share the same concurrency limit and cancellation behavior. Including ${{ github.workflow }} in your group key ensures that your concurrency settings apply only to that specific workflow [1][5][8].
Citations:
- 1: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- 2: https://docs.github.com/enterprise-cloud@latest/actions/using-jobs/using-concurrency
- 3: https://stackoverflow.com/questions/74737407/what-is-concurrency-in-github-actions
- 4: https://dev.to/ace2932/cancel-duplicate-github-actions-runs-with-one-block-and-the-one-place-you-shouldnt-2m3k
- 5: https://stackoverflow.com/questions/66335225/how-to-cancel-previous-runs-in-the-pr-when-you-push-new-commitsupdate-the-curre
- 6: Clarify concurrency cancel-in-progress behaviour github/docs#30647
- 7: https://starsling.dev/best-practices/github-actions/cancel-superseded-runs
- 8: https://docs.github.com/en/enterprise-server@3.20/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- 9: https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency
🌐 Web query:
GitHub Actions workflow concurrency group same group workflow branch runs cancelled queued cancelled older newer behavior
💡 Result:
In GitHub Actions, the concurrency group behavior depends on your configuration of the concurrency key, which ensures that only one workflow or job with a specific group name runs at any given time [1][2]. How runs are handled: 1. In-Progress Runs: If a new run starts and a previous run is already active in the same concurrency group, the behavior depends on the cancel-in-progress setting [1][3]. - When cancel-in-progress: true is set, the currently running workflow or job in that group is canceled immediately, allowing the new run to proceed [1][3]. - When cancel-in-progress: false (the default) is set, the active run is allowed to finish [3]. 2. Queued/Pending Runs: - Traditionally, GitHub Actions allowed at most one pending run in a concurrency group; if a new run was triggered while another was already waiting, the older pending run was automatically canceled and replaced by the new one [1][2][4]. - As of May 2026, you can now enable larger queues by adding queue: max to the concurrency block [4]. This allows multiple runs to wait in line and execute sequentially, rather than canceling pending runs [5][4]. Key Considerations: - Group Scope: Concurrency group names are evaluated at the repository level [1][2]. If multiple different workflows use the same group name (e.g., a shared string like my-group), they will interfere with each other and cancel/queue one another [1][6]. To scope behavior to a specific workflow or branch, it is best practice to include variables in the group name, such as ${{ github.workflow }}-${{ github.ref }} [1][6][3]. - Branch Behavior: If you include github.ref in your group name, runs on different branches will belong to different concurrency groups and will not affect each other [3]. If you use a static group name without reference to the branch, runs from all branches will share the same group and trigger cancellation or queuing across the entire repository [1][2]. Summary of cancel-in-progress vs queue: - cancel-in-progress: true: Prioritizes the newest run by killing older in-progress runs [3]. - cancel-in-progress: false (Default) + queue: max: Ensures all runs complete in sequence, preventing loss of work [4].
Citations:
- 1: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- 2: https://docs.github.com/enterprise-cloud@latest/actions/using-jobs/using-concurrency
- 3: https://starsling.dev/best-practices/github-actions/cancel-superseded-runs
- 4: https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/
- 5: https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency
- 6: GitHub Actions: Examples for canceling in-progress jobs / runs with concurrency groups lead to confusing behavior github/docs#11243
Serialize beta release publication.
build-beta.yml has no concurrency guard, so multiple beta runs can upload to beta-fw and update the same Git tag in parallel. An older run finishing after a newer one can overwrite the newer manifest/binaries and repoint the tag to an older commit. Add workflow-level concurrency with a stable beta publication group name, e.g. group: ${{ github.workflow }}-${{ github.ref }}, and either cancel-in-progress: true for the latest commit or queue: max for strict serialization.
Proposed guard
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/build-beta.yml around lines 48 - 102, Add workflow-level
concurrency to build-beta.yml using a stable beta publication group such as `${{
github.workflow }}-${{ github.ref }}`. Configure the guard to serialize or
cancel overlapping runs, ensuring only the latest beta publication can upload
assets and update the beta-fw tag; place it at the workflow level rather than
inside the publish-beta job.
| esp32_ble_tracker: | ||
| id: ble_tracker | ||
| scan_parameters: | ||
| continuous: true |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | grep -F 'Integrations/ESPHome/Core.yaml' || true
echo "== file snippet =="
if [ -f Integrations/ESPHome/Core.yaml ]; then
nl -ba Integrations/ESPHome/Core.yaml | sed -n '1,120p'
fi
echo "== esp32_ble_tracker references =="
rg -n "esp32_ble_tracker|continuous|start_scan|stop_scan|on_turn_on|on_boot|on_state" Integrations/ESPHome/Core.yaml .github 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1
Length of output: 269
🌐 Web query:
ESPHome esp32_ble_tracker continuous true starts scanning during setup documentation
💡 Result:
In ESPHome, the esp32_ble_tracker component does not start scanning immediately during the setup() method if continuous: true is configured [1]. Instead, the setup() method initializes the Bluetooth stack and FreeRTOS task, but it intentionally defers the start of the scanning process to the loop() method [1]. This design ensures that the system allows the scanner and other components to reach an idle and stable state before the scanning process begins [1]. Key technical details: - Configuration: When continuous: true is set (which is the default behavior for the component), the internal state scan_continuous_ is set to true [2][1]. - Execution: During the setup() phase, the scan_continuous_ flag is set, but the code explicitly avoids triggering an immediate scan [1]. - Loop: The loop() method, which runs repeatedly after setup, checks this flag and initiates the scanning process once it is safe to do so [3][1]. This behavior provides a safer startup sequence, preventing potential conflicts or stability issues that could arise from starting the BLE stack and scanning simultaneously with other system initialization tasks [1].
Citations:
- 1: https://api-docs-dev.esphome.io/esp32__ble__tracker_8cpp_source
- 2: https://esphome.io/components/esp32_ble_tracker/
- 3: https://github.com/esphome/esphome/blob/dev/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file section =="
awk '{ printf "%6d ", NR } { print }' Integrations/ESPHome/Core.yaml | sed -n '1,120p'
echo "== esp32_ble_tracker references in Core =="
grep -nE "esp32_ble_tracker|continuous:|start_scan|stop_scan|on_boot|on_turn_on|on_state" Integrations/ESPHome/Core.yaml || true
echo "== deterministic YAML control-flow model =="
python3 - <<'PY'
from pathlib import Path
text = Path("Integrations/ESPHome/Core.yaml").read_text()
ops = [
("component setup esp32_ble_tracker", "esp32_ble_tracker:"),
("continuous true flag", "continuous: true"),
("boot action start_scan", "on_boot:"),
("boot action stop_scan", "stop_scan"),
("switch turn_on action start_scan", "on_turn_on:"),
("switch turn_off action stop_scan", "on_turn_off:"),
]
for name, needle in ops:
idx = (text + "\n").index(needle + "\n", 0) if needle in text else -1
line = text.count("\n", 0, idx) + 1 if idx != -1 else None
print(f"{name}: line={line} found={idx != -1}")
print("order:", [(l, name) for name, line in ops if (l := text.count("\n", 0, text.index(needle))) != -1])
PYRepository: ApolloAutomation/PUMP-1
Length of output: 5310
🌐 Web query:
ESPHome esp32_ble_tracker start_scan stop_scan start_scan action
💡 Result:
In ESPHome, the esp32_ble_tracker component provides two primary actions for manual scan control: esp32_ble_tracker.start_scan and esp32_ble_tracker.stop_scan [1][2]. These actions are typically used when the component is configured with continuous: false, allowing you to trigger scanning on demand through automations [1]. esp32_ble_tracker.start_scan Action: - Function: Initiates a Bluetooth scan [1]. - Behavior: If a scan is already in progress, the action is ignored [1]. - Configuration: You can optionally specify the continuous parameter within the action, which determines if the scan should run in continuous mode (defaults to false) [1]. Example: - esp32_ble_tracker.start_scan: continuous: true esp32_ble_tracker.stop_scan Action: - Function: Terminates the current Bluetooth scan [1]. - Behavior: Once stopped, scanning can be restarted using the start_scan action [1]. Usage Context: To use these actions, your esp32_ble_tracker configuration must generally have continuous set to false if you intend to control scanning manually rather than running it continuously [1]. This is useful for battery-saving scenarios or specific event-driven logic [1]. For instance, you might start a scan when a client connects to the API and stop it when it disconnects [1].
Citations:
🌐 Web query:
ESPHome bluetooth_proxy default state BLE Proxy switch off
💡 Result:
In ESPHome, the bluetooth_proxy component is configured with an active parameter that controls whether the proxy supports active GATT connections [1][2]. As of recent updates, the default state for the active parameter is true [3][4]. To switch off active connections (effectively returning to the previous passive-only proxy behavior), you must explicitly set the active parameter to false in your YAML configuration [1][4]: bluetooth_proxy: active: false It is important to distinguish this configuration from BLE scanning settings [5]. The bluetooth_proxy: active setting only governs the ability to support active GATT connections requested by Home Assistant; it does not autonomously connect to devices and does not impact the battery life of your BLE sensors [5]. If you are looking to reduce battery drain on sensors, you should instead look at the esp32_ble_tracker component and its scan_parameters, specifically setting active: false there, as active scanning causes sensors to transmit more frequently [1][5]. There is no native "switch" entity in ESPHome to dynamically toggle the bluetooth_proxy component on or off during runtime [6][7]. If you need to completely remove the functionality, you can use the !remove package directive in your YAML configuration [8]: packages: my_proxy:!remove Alternatively, some users have implemented custom template switches using lambda actions to modify the state of the proxy programmatically, though this is an advanced workaround and not an official feature [6].
Citations:
- 1: https://esphome.io/components/bluetooth_proxy/
- 2: https://new.esphome.io/components/bluetooth_proxy/
- 3: [bluetooth_proxy] Change default for active connections to true esphome/esphome#10546
- 4: esphome/esphome.io@9dbf889e
- 5: bluetooth_proxy: Active (default) - Needs warning in documentation esphome/esphome.io#5373
- 6: https://community.home-assistant.io/t/a-switch-for-turning-bt-proxy-on-or-off/626178
- 7: https://community.home-assistant.io/t/cant-enable-esphome-bluetooth-proxy/643358
- 8: https://www.reddit.com/r/Esphome/comments/1lwg6p3/disable_proxy_feature/
🌐 Web query:
ESPHome 2025.9 bluetooth_proxy switch default OFF
💡 Result:
In ESPHome versions released after the change implemented in September 2025, the bluetooth_proxy component's active parameter defaults to true [1][2]. Prior to this update, the default behavior was false (passive mode only) [3][4]. If you wish to restore the previous behavior—where active GATT connections are disabled—you must explicitly set the parameter to false in your configuration [1][2]: bluetooth_proxy: active: false It is important to distinguish this setting from the esp32_ble_tracker scan_parameters: active setting [5]. The bluetooth_proxy: active parameter only enables the capability for the proxy to support active GATT connections when requested by Home Assistant; it does not autonomously connect to devices or impact the battery life of BLE sensors [5]. Conversely, setting esp32_ble_tracker: active: true enables active scanning, which can significantly affect battery-powered devices [5].
Citations:
- 1: https://esphome.io/components/bluetooth_proxy/
- 2: esphome/esphome.io@9dbf889e
- 3: [bluetooth_proxy] Change default for active connections to true esphome/esphome#10546
- 4: https://jirkovynavody.cz/en/homeassistant/integrations/bluetooth/bluetooth_proxy/
- 5: bluetooth_proxy: Active (default) - Needs warning in documentation esphome/esphome.io#5373
🌐 Web query:
ESPHome esp32_ble_tracker loop continuous scan_continuous_ start_scan
💡 Result:
In ESPHome, the esp32_ble_tracker component manages Bluetooth Low Energy scanning through its configuration parameters and available actions [1][2]. Configuration: The continuous parameter within scan_parameters determines the default scanning behavior [1][3]. - If continuous: true (default), the tracker scans indefinitely [1][3]. - If continuous: false, the tracker remains idle until an explicit start command is issued [1][2]. Actions: - esp32_ble_tracker.start_scan: This action triggers a BLE scan [1][2]. It is idempotent, meaning calling it while a scan is already in progress is ignored [1][4]. Recent updates ensure this action safely checks if the scanner is in an IDLE state before starting [4]. - esp32_ble_tracker.stop_scan: This action halts the scanning process [1][2]. Internal Mechanism: - The component maintains an internal state machine (e.g., IDLE, STARTING, RUNNING) to manage operations [4][5]. - The scan_continuous_ boolean variable is used internally by the class to track the intended operational mode (continuous vs. triggered) [6][7][8]. - The loop() method in the component's implementation ensures the scanner state is monitored and, if configured for continuous operation, restarts the scan if necessary (for example, after the BLE stack has been disabled and re-enabled) [5]. Common Use Case: On single-core chips (e.g., ESP32-C3) where the BLE tracker and Wi-Fi may conflict, a common configuration pattern is to set continuous: false and use esp32_ble_tracker.start_scan within automations (e.g., triggered by on_client_connected) to manage the scan only when needed, avoiding stability issues [1][3][2].
Citations:
- 1: https://esphome.io/components/esp32_ble_tracker/
- 2: https://esphome-docs.pages.dev/components/esp32_ble_tracker/
- 3: https://new.esphome.io/components/esp32_ble_tracker/
- 4: [esp32_ble_tracker] Make start_scan action idempotent esphome/esphome#12864
- 5: https://github.com/esphome/esphome/blob/dev/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp
- 6: https://api-docs.esphome.io/esp32__ble__tracker_8h_source
- 7: https://api-docs.esphome.io/classesphome_1_1esp32__ble__tracker_1_1_e_s_p32_b_l_e_tracker
- 8: https://api-docs-dev.esphome.io/classesphome_1_1esp32__ble__tracker_1_1_e_s_p32_b_l_e_tracker
Do not enable BLE scanning while the proxy switch is off.
continuous: true makes the BLE tracker scan automatically after startup. The low-priority on_boot branch stops the scan only after the default-off proxy check has already run, so BLE scanning can still occur while the switch state is OFF. Set scan_parameters.continuous to false; the existing start_scan: continuous: true actions start scanning when needed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/Core.yaml` around lines 24 - 27, Set
esp32_ble_tracker.scan_parameters.continuous to false so BLE scanning does not
begin automatically at startup while the proxy switch is off. Preserve the
existing start_scan actions that explicitly enable continuous scanning when
needed.
| - lambda: "id(testScript).execute();" | ||
|
|
||
| api: | ||
| encryption: |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate relevant file =="
git ls-files | grep -F 'Integrations/ESPHome/Core.yaml' || true
echo
echo "== Core.yaml lines around encryption =="
if [ -f 'Integrations/ESPHome/Core.yaml' ]; then
nl -ba 'Integrations/ESPHome/Core.yaml' | sed -n '65,90p'
fi
echo
echo "== Inspect API component occurrences/usages in ESPHome configs =="
rg -n "api:|encryption:|key:" Integrations/ESPHome || true
echo
echo "== Search for PR title/objective references ="
git diff --stat || trueRepository: ApolloAutomation/PUMP-1
Length of output: 301
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Read Core.yaml lines around encryption =="
awk 'NR>=65 && NR<=90 { printf "%6d\t%s\n", NR, $0 }' Integrations/ESPHome/Core.yaml
echo
echo "== ESPHome API/encryption occurrences =="
grep -RInE 'api:|encryption:|key:' Integrations/ESPHome || true
echo
echo "== Read ESPHome API config context =="
grep -RInE -C 8 '^api:| encryption:|api:' Integrations/ESPHome || trueRepository: ApolloAutomation/PUMP-1
Length of output: 2176
🌐 Web query:
ESPHome API encryption config empty encryption key API plaintext unencrypted
💡 Result:
In ESPHome, the encryption configuration for the native API is optional [1][2]. If you do not include the encryption block in your api: configuration, the API will operate in plaintext (unencrypted) mode [3]. Regarding the key parameter within the encryption block: - If the encryption block is present, a 32-byte base64-encoded key is required [1][2]. - Providing an empty encryption block or an empty key is not a valid configuration for enabling encryption [1][2]. If the key is not provided or is invalid, the API will not function securely as intended. - If you intend to use encryption, you must provide a valid 32-byte base64-encoded string [1]. In summary, if you want unencrypted, plaintext communication, simply omit the entire encryption configuration block from your api: settings [1][2]. If you want encryption, you must include the encryption block with a valid, non-empty key [1][2]. The older password authentication method is deprecated and should not be used in combination with encryption [4][5].
Citations:
- 1: https://esphome.io/components/api/
- 2: https://community.home-assistant.io/t/esphome-issue-with-api-encryption-when-using-web-esphome-vs-using-the-install-option-on-the-windows-machine-that-is-connecting-to-my-pi-through-network/837389
- 3: https://developers.esphome.io/architecture/api/protocol_details/
- 4: https://new.esphome.io/components/api/
- 5: [api] Make password and encryption mutually exclusive, add deprecation warning for password auth esphome/esphome#10871
Configure a valid API encryption key.
The empty encryption: block does not enable ESPHome API encryption; the API still runs plaintext unless encryption.key is provided. Add a securely provisioned 32-byte base64-encoded key to meet the encryption objective.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/Core.yaml` at line 81, Update the ESPHome API encryption
configuration under the encryption block to provide encryption.key with a
securely provisioned, valid 32-byte base64-encoded value. Keep the existing
encryption configuration structure and ensure the key is not empty.
| - 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); | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files (selected):"
git ls-files | rg '(^|/)Core\.ya?ml$|(^|/)ESPHome/|integration' || true
echo
echo "File outline/size:"
wc -l Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Relevant sections:"
sed -n '130,175p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
sed -n '455,500p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Search for update/select/ota/manifest symbols:"
rg -n "update_http_request|apply_ota_source|update_firmware|ota|select|channel|available|force|version" Integrations/ESPHome/Core.yaml 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1
Length of output: 4720
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files (selected):"
git ls-files | rg '(^|/)Core\.ya?ml$|(^|/)ESPHome/|integration' || true
echo
echo "File outline/size:"
wc -l Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Relevant sections:"
sed -n '130,175p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
sed -n '455,500p' Integrations/ESPHome/Core.yaml 2>/dev/null || true
echo
echo "Search for update/select/ota/manifest symbols:"
rg -n "update_http_request|apply_ota_source|update_firmware|ota|select|channel|available|force|version" Integrations/ESPHome/Core.yaml 2>/dev/null || trueRepository: ApolloAutomation/PUMP-1
Length of output: 4720
🌐 Web query:
ESPHome component.update fetches manifest in separate ESP32 task perform true cached url md5
💡 Result:
In ESPHome, the http_request update component manages firmware updates by fetching a JSON manifest file from a specified URL [1][2]. On ESP32 devices, the manifest fetching process is designed to run in a separate FreeRTOS task to prevent blocking the main loop while performing network operations [3][4]. Regarding your specific technical queries: Separate ESP32 Task: The http_request update component utilizes a dedicated task (often referred to as update_task) on ESP32-based devices to perform the manifest fetch and associated HTTP operations [3][4]. This architecture allows the device to remain responsive while checking for updates [4]. Cached/MD5: The update component uses the manifest to check for new firmware versions based on metadata [1][2]. It requires the manifest to provide an MD5 hash for the firmware binary [1][5]. This MD5 hash is used during the subsequent flashing process (initiated by the user via the update.perform action) to verify the integrity of the downloaded firmware binary [4][6]. If the MD5 hash of the downloaded file does not match the value provided in the manifest, the update is rejected, and the device continues to run the existing firmware [6]. The update component itself does not inherently implement generic HTTP-level caching for the manifest file, though standard network stack behavior may apply. Note that caching mechanisms (like ETag or Last-Modified) are more explicitly documented for other components, such as online_image [7][8]. If you are encountering issues with this process, such as failed fetches at startup, it is often because the component attempts to reach the network before the connection is fully established [9]. This has been addressed in recent updates to ensure checks are skipped if the network is not yet connected [9].
Citations:
- 1: https://esphome.io/components/update/http_request/
- 2: [core] Update Entities esphome/esphome#6885
- 3: https://api-docs-dev.esphome.io/http__request__update_8h_source
- 4: https://api-docs.esphome.io/http__request__update_8cpp_source
- 5: https://new.esphome.io/components/update/http_request/
- 6: https://esphome.io/components/ota/http_request/
- 7: https://esphome.io/components/online_image/
- 8: https://esphome.io/components/image/online_image/
- 9: Managed Updates attempt to fetch manifest before network is connected esphome/esphome#12416
Synchronize manifest refresh before forcing installation.
component.update: update_http_request schedules the manifest fetch in a separate ESP32 task, so the five-second wait can expire while the fetch is still running. perform(true) will then install from whatever manifest metadata is currently committed, which may still belong to the previously selected Stable channel after the user changed the select to Beta. Use a completion callback/state machine before forcing the install, and apply the same pattern to the on_value path for firmware_channel.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/Core.yaml` around lines 146 - 160, Update the Firmware
Update flow around update_firmware and the firmware_channel on_value handler so
manifest retrieval is synchronized through a completion callback or explicit
state machine before invoking update_http_request.perform(true). Remove reliance
on the fixed five-second delay, ensure installation uses metadata fetched for
the newly selected channel, and apply the same synchronization behavior to both
update triggers.
Version: 26.7.12.1
What does this implement/fix?
Rebuild of #45 on the unified pattern MSR-1 shipped as 26.7.9.1 (ApolloAutomation/MSR-1#100, ApolloAutomation/MSR-1#103, ApolloAutomation/MSR-1#104). Everything #45 did, plus the fix set that landed on MSR-1 afterward:
bluetooth_proxy+esp32_ble_trackernow compile into every image with a "Bluetooth Proxy" switch (default off, persisted, re-applied at boot). PUMP-1 never had a BLE proxy offering; as an always-on C6/8MB device it is a clean proxy candidate, and the switch defaults off so nothing changes until a user opts in.apply_ota_source: a directset_source_urlswap fromota_stable_manifest/ota_beta_manifestsubstitutions. The channel is now also re-applied at boot (on_boot -100), so a rebooted device keeps pointing at its selected channel.#ifdef USE_ESP32_BLEdisable/re-enable lambdas from Add Firmware Channel switching and serve the end-user image for OTA #45's update button are gone — ESPHome's OTA quiesces BLE itself (MSR-1 #103).http_requestconsolidated into Core.yaml withbuffer_size_rx: 5120/buffer_size_tx: 2048— GitHub release redirects overflow the 512-byte defaults (~3.6 KB CSP header line, ~850-char signed query string). Add Firmware Channel switching and serve the end-user image for OTA #45 had no buffer sizing; beta-channel OTA fails without it.on_bootconverted from mapping to list form. Mapping form replaces Core's wholeon_bootlist in the package merge, so the pump-safety boot init and refill-mode application never ran on the Minimal image. That was latent while Minimal was just the adoption stub; it matters now that Minimal is the fielded OTA image.firmware/; the improv image moves tofirmware-factory/;static/index.htmlrepointed. install.apolloautomation.com needs the matching repoint when this ships to main (mirrors Point MSR-1 installer at the factory image (restores Wi-Fi setup step) installer#16).beta-channel/wrapper + build-beta.yml publishing to the rollingbeta-fwpre-release.min_version2023.11.1 → 2025.11.0 (the update-system floor set on R_PRO-1); version 26.7.12.1.Supersedes #45. Leaves #37, #35, and #14 alone.
Types of changes
Checklist / Checklijst:
If user-visible functionality or configuration variables are added/modified:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements