From f7a71c1fd5f65cc80f918906bffcb6af1a9dd3a0 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:09:49 -0500 Subject: [PATCH 1/2] Trim verbose comments in Core.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cuts the comment blocks down to the non-obvious facts and drops comments that restate the identifier below them. Three corrections beyond the trim: - Move the BT PSRAM comment into the sdkconfig block, above CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST, instead of leaving it above esp32_ble_tracker where it read as describing the tracker. - Reword the resampler stack comment as a "must", since it guards the deliberate absence of task_stack_in_psram rather than a setting present in the file. - Drop the commented-out decode_memory line, which parked a disabled option in the config instead of stating a fact. Comments only. esphome config validates unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 118 +++++++++------------------------ 1 file changed, 32 insertions(+), 86 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 083db49..7ddfcc8 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,6 +1,5 @@ substitutions: version: "26.8.28.1" - # Manifest bases: stable=Pages, beta=release assets stable_manifest_base: "https://apolloautomation.github.io/CAST-1" beta_manifest_base: "https://github.com/ApolloAutomation/CAST-1/releases/download/beta-fw" stable_manifest_dir: "firmware" @@ -27,13 +26,9 @@ esphome: - lambda: "return id(runTest);" then: - lambda: "id(testScript).execute();" - # A device whose WiFi was released while on Ethernet boots with WiFi - # (and its hotspot) off: with no STA credentials the fallback AP would - # otherwise broadcast until Ethernet gets its DHCP lease. Runs after - # globals restore (800), before wifi setup (245). Safe mode skips - # on_boot entirely, so WiFi always starts there for recovery. ESP-NOW - # self-inits on channel 1 (legacy Ethernet-firmware default) when WiFi - # is off at setup. + # Keep WiFi off when Ethernet is expected, so the fallback hotspot can't + # broadcast while Ethernet waits for DHCP + # Must run after globals restore (800) and before wifi setup (245) - priority: 600 then: - if: @@ -43,11 +38,9 @@ esphome: - lambda: |- id(wifi_1).set_enable_on_boot(false); id(espnow_component).set_wifi_channel(1); - # No Ethernet HAT: the W5500 driver's helper task is created before chip - # verification and survives the failed install; left running it polls - # the floating SPI bus and eventually corrupts memory (Cache error - # panics every ~100s, observed on hardware). Park it. Runs right after - # ethernet's setup (250). + # Park the W5500 helper task when no HAT is present, so it stops polling a + # floating SPI bus and panicking the device every ~100s + # Must run after ethernet setup (250) - priority: 200 then: - if: @@ -65,12 +58,10 @@ esphome: - text_sensor.template.publish: id: apollo_firmware_version state: "${version}" - # Point update entity at selected manifest - priority: -100 then: - script.execute: apply_ota_source - # Ethernet was expected but never came up: bring WiFi back so the - # device can rejoin a network or open the provisioning hotspot. + # Ethernet never came up, so bring WiFi back for rejoin or provisioning - priority: -100 then: - if: @@ -88,7 +79,6 @@ esphome: then: - lambda: "id(ethernet_expected) = false;" - wifi.enable: - # Re-apply persisted Bluetooth Proxy state - priority: -300 then: - if: @@ -118,8 +108,7 @@ update: name: Firmware Update source: ${stable_manifest_base}/${stable_manifest_dir}/manifest.json -# Both interfaces live in one firmware (ESPHome 2026.8+); Ethernet is -# preferred whenever connected and ESPHome arbitrates the default route. +# Prefer Ethernet whenever it is connected; ESPHome arbitrates the route network: id: network_1 priority: @@ -137,12 +126,8 @@ ethernet: interrupt_pin: GPIO13 reset_pin: GPIO21 on_connect: - # Ethernet is up. If WiFi is (or is about to be) connected to a network, - # keep it: the device runs dual-homed — Ethernet carries the routed - # traffic (network: priority:) while Home Assistant's connection to the - # WiFi address survives, so no rediscovery is ever needed. WiFi is turned - # off only when it has nothing to offer (hotspot active or nothing to - # join), which also guarantees the hotspot is never up while on Ethernet. + # Keep WiFi up alongside Ethernet so Home Assistant's connection to the + # WiFi address survives; release it only when WiFi has no network to offer - if: condition: wifi.enabled: @@ -154,11 +139,8 @@ ethernet: # Fallback hotspot is broadcasting: WiFi has no network. - script.execute: wifi_release_radio else: - # Boot race: WiFi may still be associating; give it a moment. - # The wait also ends the instant the fallback AP starts (WiFi - # lost 60-90s before the cable arrived), so the hotspot is - # torn down within a loop pass instead of broadcasting for - # the rest of the window. + # WiFi may still be associating at boot, so wait it out + # Ends early if the fallback AP starts - wait_until: condition: or: @@ -171,8 +153,7 @@ ethernet: then: - lambda: "id(ethernet_expected) = false;" else: - # Guard: if the cable vanished during the wait, leave - # WiFi alone — it is the only path left. + # Leave WiFi alone if the cable vanished during the wait - if: condition: ethernet.connected: @@ -184,8 +165,7 @@ ethernet: - component.update: update_http_request - component.update: network_connection on_disconnect: - # Cable gone: make sure WiFi is up so the device rejoins a saved network - # or, with none configured, opens the provisioning hotspot. + # Cable gone, so bring WiFi up to rejoin or open the provisioning hotspot - lambda: "id(ethernet_expected) = false;" - wifi.enable: - component.update: network_connection @@ -193,19 +173,13 @@ ethernet: wifi: id: wifi_1 on_connect: - # Remember that working credentials exist: gates the recovery probe - # below so it never runs (and can never pop the hotspot) on a device - # that was never provisioned. Survives reboots; factory reset clears it. + # Must stay set: gates the recovery probe so it never pops the hotspot on + # a device that was never provisioned - lambda: "id(wifi_provisioned) = true;" - component.update: update_http_request on_disconnect: - # WiFi lost its network while Ethernet carries the device: after up to - # 60s for reassociation, turn WiFi off rather than let the fallback - # hotspot appear (ap_timeout is 90s; deciding by 60s keeps it from ever - # starting). A reconnect-then-drop during the wait can release earlier - # than the full 60s — the 10-minute recovery probe restores dual-home in - # that case. Harmless when triggered by wifi.disable itself — the - # wifi.enabled check below skips the release then. + # Turn WiFi off after 60s without reassociation, which keeps the 90s + # fallback hotspot from ever starting while Ethernet carries the device - if: condition: ethernet.connected: @@ -254,13 +228,13 @@ esp32: CONFIG_SPIRAM_RODATA: "y" CONFIG_SPIRAM_FETCH_INSTRUCTIONS: "y" + # Allocate BT in PSRAM so internal RAM stays free for audio CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST: "y" CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY: "y" CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC: "y" CONFIG_MBEDTLS_SSL_PROTO_TLS1_3: "y" -# BT allocs in PSRAM; RAM free for audio esp32_ble_tracker: id: ble_tracker scan_parameters: @@ -271,11 +245,8 @@ bluetooth_proxy: api: id: api_1 - # A CAST-1 can legitimately run without a Home Assistant API client — - # streaming from Music Assistant uses sendspin's own outbound connection. - # The default 15-minute no-client reboot would restart such a device mid- - # playback every cycle (observed in the field), so it is disabled; crash - # recovery remains covered by safe_mode. + # Disable the no-client reboot: sendspin streams from Music Assistant over + # its own connection, so a device with no API client is normal here reboot_timeout: 0s encryption: @@ -299,16 +270,13 @@ globals: - id: announcement_triggered type: bool initial_value: 'false' - # True when WiFi was released while Ethernet carried the device (WiFi had - # nothing to offer); the next boot then starts WiFi-silent. Cleared when - # the cable is lost, Ethernet fails to appear at boot, or the device goes - # dual-homed. + # True when WiFi was released while Ethernet carried the device, which makes + # the next boot start WiFi-silent - id: ethernet_expected type: bool restore_value: yes initial_value: 'false' - # True once WiFi has ever connected (credentials known-good). Gates the - # WiFi recovery probe. Cleared only by factory reset. + # True once WiFi has ever connected, which gates the WiFi recovery probe - id: wifi_provisioned type: bool restore_value: yes @@ -426,10 +394,7 @@ switch: - esp32_ble_tracker.stop_scan: text_sensor: - # Which path is carrying the device right now — for troubleshooting and - # HA notifications (e.g. alert when a wired device falls back to WiFi). - # Ethernet + WiFi = dual-home: traffic on the wire, WiFi held so Home - # Assistant never loses the device. + # Which path is carrying the device, for troubleshooting and HA notifications - platform: template name: "Network Connection" id: network_connection @@ -559,7 +524,7 @@ speaker: - id: media_mixer_input timeout: never - # Hot audio tasks: stacks stay in internal RAM + # These stacks must stay in internal RAM, so leave task_stack_in_psram unset - platform: resampler id: announcement_resampling_speaker output_speaker: announcement_mixer_input @@ -581,7 +546,6 @@ sendspin: media_source: - platform: sendspin id: sendspin_source - # decode_memory: component default - platform: audio_http id: http_media_source @@ -666,11 +630,8 @@ media_player: -# A released WiFi (router outage while dual-homed, or slow association at -# cable-connect) must not stay silent until the next cable pull: while -# Ethernet is up and the device has known-good credentials, retry WiFi -# periodically. The 60s bound stays under the 90s hotspot timer (which -# restarts at wifi.enable), so the probe can never broadcast the AP. +# Retry a released WiFi while Ethernet is up, so an outage doesn't leave the +# radio silent until the next cable pull interval: - id: wifi_recovery_interval interval: 10min @@ -708,30 +669,16 @@ script: - script.execute: wifi_release_radio - id: wifi_release_radio - # Turn WiFi off while Ethernet carries the device: ends AP+STA, shuts - # down the captive portal (wifi.disable leaves it answering DNS and - # /wifisave on the Ethernet IP), and bounces ESP-NOW so it re-inits - # standalone for the WizMote (the channel scan in wizmote.yaml takes - # over). ethernet_expected persists so the next boot starts WiFi-silent. - # - # If the fallback hotspot is LIVE, reboot instead: ESPHome cannot - # relaunch a once-started AP within the same boot (ap_setup_ latch), so - # merely disabling WiFi would leave the next cable-out provisioning - # attempt with a half-dead hotspot (SSID joins, but 192.168.4.1 serves - # nothing — observed on hardware). The reboot lands Ethernet-only in - # seconds with the latch cleared. It fires only when a live hotspot must - # be killed: a fresh device's first cable-connect, or an unprovisioned - # wired device whose cable was out long enough (>90s) for the hotspot to - # appear — in both cases the device had no network, so no playback is - # interrupted. A device on a regularly-failing switch repeats this per - # outage cycle; kept in preference to suppressing the cable-out hotspot, - # which is a documented provisioning path. + # Turn WiFi off while Ethernet carries the device, ending AP+STA and the + # captive portal, and bounce ESP-NOW so it re-inits standalone for the WizMote then: - lambda: "id(ethernet_expected) = true;" - if: condition: wifi.ap_active: then: + # Must reboot to clear a live hotspot: ESPHome can't relaunch a + # once-started AP in the same boot, leaving it serving nothing - lambda: |- ESP_LOGW("network", "Rebooting to fully release the active hotspot (one-time)"); - delay: 500ms @@ -746,7 +693,6 @@ script: id(espnow_component).enable(); - id: apply_ota_source - # Set OTA manifest from the channel selector then: - lambda: |- const bool beta = id(firmware_channel).current_option() == "Beta"; From 9f77e8ced44a23a52706d7a015e137d1e5394eff Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:19:47 -0500 Subject: [PATCH 2/2] Trim comments in CAST-1.yaml and wizmote.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pass as Core.yaml, applied to the other two device YAMLs. CAST-1.yaml's header repeated the Ethernet preference that Core.yaml now states at the network block; what is left names the file and points at Core.yaml. wizmote.yaml's channel-scan block reduces to the rule it guards. Comments only. esphome config validates unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/CAST-1.yaml | 5 ++--- Integrations/ESPHome/wizmote.yaml | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Integrations/ESPHome/CAST-1.yaml b/Integrations/ESPHome/CAST-1.yaml index 96d0880..bd5fc4a 100644 --- a/Integrations/ESPHome/CAST-1.yaml +++ b/Integrations/ESPHome/CAST-1.yaml @@ -1,6 +1,5 @@ -# Apollo CAST-1 — unified firmware with WiFi + Ethernet in one image. -# Ethernet is preferred when plugged in; WiFi (and its provisioning hotspot) -# takes over automatically when it is not. See Core.yaml for all device config. +# Apollo CAST-1 unified firmware: WiFi and Ethernet in one image +# Device config lives in Core.yaml esphome: name: "apollo-cast-1" diff --git a/Integrations/ESPHome/wizmote.yaml b/Integrations/ESPHome/wizmote.yaml index dbf4765..538b7ac 100644 --- a/Integrations/ESPHome/wizmote.yaml +++ b/Integrations/ESPHome/wizmote.yaml @@ -388,10 +388,8 @@ script: format: "unmapped button %d" args: ['button'] -# When WiFi has been released (device on Ethernet with no WiFi network), -# ESP-NOW owns the radio: scan channels until the WizMote is heard. While -# WiFi is enabled the channel is pinned by the WiFi connection, so scanning -# is skipped (and espnow.set_channel would be refused anyway). +# Scan channels for the WizMote once ESP-NOW owns the radio +# Must skip while WiFi is enabled, which pins the channel interval: - id: wizmote_channel_scan interval: 2s