From 1d4d71cfa7f2ee493512dd2ad5086bf7ecafa08e Mon Sep 17 00:00:00 2001 From: Justin Bunton <130115204+Bunton33@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:27:22 -0400 Subject: [PATCH] Add song 6, Improv over BLE, and the certification fixes Bumps the version to 26.8.25.1 so the update entity offers this to units in the field - the shared build workflow reads that substitution out of Core.yaml as the release tag. Song 6: "Do You Want To Build A Snowman" Double-press or hold on Song Button 2 (GPIO5) plays the Frozen chorus hook. Song 1 is already called "Snowman" but is Frosty the Snowman, a different tune, so this one is named BuildASnowman to keep the two apart in the rtttl logs. 105 chars, 34 notes, 11.9s, well inside the ~254 byte NVS preference. Button 2 moves from a plain on_release to the same on_multi_click set buttons 1 and 3 use, and the deep-sleep wake decoder gets the matching hold-confirm path. A double-press cannot survive deep sleep - boot from the ext1 wake outlasts the two clicks and ext1 gives a latched pin mask with no click count - so the hold is what makes the gesture work asleep, exactly as on the other two buttons. Button 2 gives up its zero-latency wake path for this. Also adds a Snowfall LED effect so song 6 has its own look, clock-driven rather than positional for the same reason PinkParty is. Improv over BLE esp32_improv was the one hard Made for ESPHome requirement the config was missing. It costs +432 B RAM and +21 KB flash here because esp32_ble is already linked in for the Bluetooth proxy, and it stops itself once wifi connects, so a provisioned unit never advertises. Certification and entity quality - api: bare encryption: with no key, so Noise support is compiled in without putting a secret in a public config. An adopting client supplies the key. - ids on the singleton components (api, logger, web_server, captive_portal, improv_serial, safe_mode, http_request, bluetooth_proxy) so adopters can !extend and !remove them. - esp32_ble_tracker scan_parameters continuous: false. The tracker starts a scan in its own setup(), before the on_boot -300 handler reads the proxy switch, so a fresh unit scanned briefly with the switch off - in the same window Improv needs to advertise. - Restart, Restart (Safe Mode) and Factory Reset buttons. Factory reset was internal: true, so a 10s button hold was the only way to clear a unit. - wifi_info and version diagnostics, all disabled_by_default. - entity_category on Song Button 1/2/3, Wake-up Button Pressed, OTA Mode and Volume; they were landing on users' dashboards. - The template button was renamed to "Reinstall Firmware" - the update entity already had the name "Firmware Update". - api services: -> actions:, and device_class: firmware on the update entity. Bug fixes found along the way - A malformed RTTTL string left all 37 LEDs on at 80% forever. The play scripts light up before rtttl.play, which returns early on a parse error without ever reaching RUNNING, so on_finished_playback never fired. Song Text is user-editable, so one typo stranded the ornament lit. - Sleep Duration min_value was 0, but the on_value lambda refuses 0, so setting it left the entity reading "0 h" while the device kept sleeping for its previous duration. - swallow_clear and sleep_grace_period defaulted to mode: single, which discards a re-arm instead of restarting the window. - A stray ';' on songs 2 and 3 parsed as a rest, appending a silent quarter note to each. Verified with a clean build on esphome 2026.7.3: H-3 at RAM 47.3% / flash 51.0%, H-3D at RAM 18.6% / flash 8.5%. Co-Authored-By: Claude Opus 5 --- Integrations/ESPHome/Core.yaml | 235 +++++++++++++++++++++++++++++++-- Integrations/ESPHome/H-3.yaml | 134 +++++++++++++++++-- README.md | 2 +- static/_config.yml | 4 +- 4 files changed, 355 insertions(+), 20 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 553b93a..73f35a7 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.8.23.1" + version: "26.8.25.1" run_duration_default: "90s" run_duration_gpio: "90s" @@ -101,43 +101,62 @@ button: id: factory_reset_button name: "Factory Reset" entity_category: diagnostic - internal: true + # Was internal: true, i.e. a 10s hold on the reset button was the only way + # to clear a unit. Exposed but disabled_by_default so it cannot be hit by + # accident from a dashboard, while still being available to a user who is + # deliberately handing the ornament on. + disabled_by_default: true - platform: template id: btn_play_song_1 name: "Play Song 1" + icon: mdi:music-note on_press: then: - script.execute: play_song_1 - platform: template id: btn_play_song_2 name: "Play Song 2" + icon: mdi:music-note on_press: then: - script.execute: play_song_2 - platform: template id: btn_play_song_3 name: "Play Song 3" + icon: mdi:music-note on_press: then: - script.execute: play_song_3 - platform: template id: btn_play_song_4 name: "Play Song 4" + icon: mdi:music-note on_press: then: - script.execute: play_song_4 - platform: template id: btn_play_song_5 name: "Play Song 5" + icon: mdi:music-note on_press: then: - script.execute: play_song_5 + - platform: template + id: btn_play_song_6 + name: "Play Song 6" + icon: mdi:music-note + on_press: + then: + - script.execute: play_song_6 number: - platform: template name: "Sleep Duration" id: deep_sleep_sleep_duration - min_value: 0 + # 1, not 0. The on_value lambda below refuses 0 (a 0us timer wake is an + # unrecoverable boot loop), so a settable 0 only produced an entity that + # read "0 h" while the device went on sleeping for the previous duration. + min_value: 1 max_value: 800 step: 1 mode: box @@ -146,7 +165,7 @@ number: restore_value: true initial_value: 24 icon: "mdi:arrow-collapse-right" - entity_category: CONFIG + entity_category: "config" unit_of_measurement: "h" on_value: then: @@ -166,6 +185,8 @@ number: initial_value: 50 unit_of_measurement: "%" step: 1 + icon: "mdi:volume-high" + entity_category: "config" optimistic: true restore_value: true on_value: @@ -176,6 +197,7 @@ text_sensor: id: wakeup_button_pressed name: "Wake-up Button Pressed" icon: "mdi:gesture-tap-button" + entity_category: "diagnostic" text: - platform: template @@ -195,7 +217,7 @@ text: restore_value: true mode: text optimistic: true - initial_value: "rudolph:d=4,o=5,b=125:8g.,16a,8p,16g.,e,c6,a,2g.,8g.,16a,8g.,16a,g,c6,1b,8f.,16g,8p,16f.,d,b,a,2g.,8g.,16a,8g.,16a,g,a,1e,8g.,16a,8p,16g.,e,c6,a,1g,8g.,16a,8g.,16a,g,c6,1b,8f.,16g,8p,16f.,d,b,a,1g,8g.,16a,8g.,16a,g,d6,2c6;" + initial_value: "rudolph:d=4,o=5,b=125:8g.,16a,8p,16g.,e,c6,a,2g.,8g.,16a,8g.,16a,g,c6,1b,8f.,16g,8p,16f.,d,b,a,2g.,8g.,16a,8g.,16a,g,a,1e,8g.,16a,8p,16g.,e,c6,a,1g,8g.,16a,8g.,16a,g,c6,1b,8f.,16g,8p,16f.,d,b,a,1g,8g.,16a,8g.,16a,g,d6,2c6" - platform: template name: "Song Text 3" id: song_3 @@ -204,7 +226,7 @@ text: restore_value: true mode: text optimistic: true - initial_value: "jingle:d=8,o=5,b=120:e6,e6,e6.,16p,e6,e6,e6.,16p,e6,g6,c6.,16d6,e6.,p,f6,f6,f6.,16f6,f6,e6,e6,16e6,16e6,e6,d6,d6,e6,d6.,16p,g6,p,e6,e6,e6.,16p,e6,e6,e6.,16p,e6,g6,c6.,16d6,2e6,f6,f6,f6.,16f6,f6,e6,e6,16e6,16e6,g6,g6,f6,d6,2c6;" + initial_value: "jingle:d=8,o=5,b=120:e6,e6,e6.,16p,e6,e6,e6.,16p,e6,g6,c6.,16d6,e6.,p,f6,f6,f6.,16f6,f6,e6,e6,16e6,16e6,e6,d6,d6,e6,d6.,16p,g6,p,e6,e6,e6.,16p,e6,e6,e6.,16p,e6,g6,c6.,16d6,2e6,f6,f6,f6.,16f6,f6,e6,e6,16e6,16e6,g6,g6,f6,d6,2c6" - platform: template name: "Song Text 4" id: song_4 @@ -224,6 +246,19 @@ text: optimistic: true # Hook looped 3x: 24.0s, 247 of the 254 chars the NVS preference holds. initial_value: "Barbie Girl:o=5,d=8,b=125,b=125:g#,e,g#,c#6,4a,4p,f#,d#,f#,b,4g#,f#,e,4p,e,c#,4f#,4c#,4p,f#,e,4g#,4f#,4p,g#,e,g#,c#6,4a,4p,f#,d#,f#,b,4g#,f#,e,4p,e,c#,4f#,4c#,4p,f#,e,4g#,4f#,4p,g#,e,g#,c#6,4a,4p,f#,d#,f#,b,4g#,f#,e,4p,e,c#,4f#,4c#,4p,f#,e,4g#,4f#" + - platform: template + name: "Song Text 6" + id: song_6 + icon: mdi:music-note + entity_category: "config" + restore_value: true + mode: text + optimistic: true + # Frozen chorus: hook / answer / hook / closing line. 11.9s, 105 of the + # 254 chars the NVS preference holds. Song 1 is "Frosty the Snowman" - a + # different tune - so this one is named BuildASnowman to keep the two + # apart in the rtttl logs. + initial_value: "BuildASnowman:d=8,o=5,b=136:g,g,g,d,g,b,4a,2b,8p,g,g,d,g,b,2a,4p,g,g,g,d,g,b,4a,2b,8p,g,g,g,g,d,g,b,4a,1g" binary_sensor: - platform: gpio @@ -281,6 +316,7 @@ binary_sensor: pulldown: true allow_other_uses: true name: "Song Button 1" + entity_category: "diagnostic" # Debounce - required by the multi-click patterns. filters: - delayed_on_off: 20ms @@ -337,18 +373,54 @@ binary_sensor: pulldown: true allow_other_uses: true name: "Song Button 2" + entity_category: "diagnostic" filters: - delayed_on_off: 20ms on_press: then: - script.execute: dismiss_first_boot - on_release: + # Same rule as buttons 1 and 3: the inter-click gap (0.4s) MUST stay shorter + # than the single-click wait (0.5s), or a second press in the overlap fires + # both. Converted from a plain on_release when song 6 was added, so a single + # click now waits out that 0.5s before playing - matching the other two. + on_multi_click: + # Double click -> song 6 + - timing: + - ON for at most 1s + - OFF for at most 0.4s + - ON for at most 1s + - OFF for at least 0.2s + then: + - if: + condition: + lambda: 'return !id(swallow_button_action);' + then: + - script.execute: play_song_6 + # Single click -> song 2 + - timing: + - ON for at most 1s + - OFF for at least 0.5s then: - if: condition: lambda: 'return !id(swallow_button_action);' then: - script.execute: play_song_2 + # Hold -> play_song_6. Same gesture the deep-sleep wake path uses, so it is + # learned once and works asleep or awake. A single open-ended timing takes + # a fast path in MultiClickTriggerBase that fires mid-hold; the two patterns + # above cap ON at 1s and have already invalidated, so exactly one fires. + # invalid_cooldown: 0s is load-bearing - the default 1s would block matching + # after a short tap, making "tap, then hold" (the obvious retry) do nothing. + - timing: + - ON for at least ${hold_gesture_length} + invalid_cooldown: 0s + then: + - if: + condition: + lambda: 'return !id(swallow_button_action);' + then: + - script.execute: play_song_6 - platform: gpio id: back_button_3 @@ -359,6 +431,7 @@ binary_sensor: pulldown: true allow_other_uses: true name: "Song Button 3" + entity_category: "diagnostic" filters: - delayed_on_off: 20ms on_press: @@ -706,6 +779,28 @@ light: (int)(105.0f * tint * breath), (int)((255.0f - 75.0f * tint) * breath)); } + - addressable_lambda: + name: "Snowfall" + update_interval: 60ms + lambda: |- + // Clock-driven like PinkParty, for the same reason: the nose is a + // single LED and the eyes are two, so anything keyed off + // i/it.size() freezes those segments on one flat colour. + const uint32_t now = millis(); + // Pale ice blue that drifts brighter and dimmer over ~4s. + float drift = 0.5f + 0.5f * sinf((now % 4000u) / 4000.0f * 6.28318530718f); + // color_correct 40% x brightness 80% = ~0.32, so keep the base high + // or the blue reads as dark grey on the wire. + int base_r = (int)(70.0f + 40.0f * drift); + int base_g = (int)(120.0f + 50.0f * drift); + + for (int i = 0; i < it.size(); i++) { + if (rand() % 100 < 7) { // a flake catches the light + it[i] = Color(255, 255, 255); + } else { + it[i] = Color(base_r, base_g, 255); + } + } # Segment views into the chain. Effects run per segment - an # addressable_lambda sees only that segment's LEDs. @@ -782,6 +877,9 @@ script: # already elapsed and latched next_enter_deep_sleep_, so releasing it here # would sleep the unit on the next loop. Give the press a normal wake window. - id: sleep_grace_period + # restart, not the default single: a second execute must push the window + # out, not be dropped on the floor. + mode: restart then: - delay: ${run_duration_gpio} - script.execute: resumeSleep @@ -792,6 +890,10 @@ script: # the handler - a flag the handler clears leaks whenever the expected pattern # never matches, and would then swallow the NEXT press. - id: swallow_clear + # restart, not the default single. dismiss_first_boot and wake_decoder can + # both arm this on the same press; with mode single the second execute was + # discarded and the swallow window ended early. + mode: restart then: - delay: 2s - lambda: "id(swallow_button_action) = false;" @@ -840,12 +942,31 @@ script: id(wakeup_button_pressed).publish_state("0"); } } - # Button 2 has no alternate song, so it keeps the old zero-latency path. - if: condition: lambda: 'return id(wake_button) == 2;' then: - - button.press: btn_play_song_2 + - wait_until: + condition: + lambda: 'return id(back_button_2).has_state();' + timeout: 250ms + - if: + condition: + binary_sensor.is_on: back_button_2 + then: + - wait_until: + condition: + binary_sensor.is_off: back_button_2 + timeout: ${wake_hold_confirm} + - if: + condition: + binary_sensor.is_on: back_button_2 + then: + - button.press: btn_play_song_6 + else: + - button.press: btn_play_song_2 + else: + - button.press: btn_play_song_2 - if: condition: lambda: 'return id(wake_button) == 1;' @@ -934,6 +1055,16 @@ script: - rtttl.play: rtttl: !lambda 'return id(song_1).state;' - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off - id: play_song_2 then: @@ -960,6 +1091,16 @@ script: - rtttl.play: rtttl: !lambda 'return id(song_2).state;' - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off - id: play_song_3 then: @@ -986,6 +1127,16 @@ script: - rtttl.play: rtttl: !lambda 'return id(song_3).state;' - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off - id: play_song_4 then: @@ -1012,6 +1163,16 @@ script: - rtttl.play: rtttl: !lambda 'return id(song_4).state;' - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off - id: play_song_5 then: @@ -1038,6 +1199,52 @@ script: - rtttl.play: rtttl: !lambda 'return id(song_5).state;' - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off + + - id: play_song_6 + then: + - light.turn_on: + brightness: 80% + id: body_light + effect: "Snowfall" + - light.turn_on: + brightness: 80% + id: mouth_light + effect: "Snowfall" + - light.turn_on: + brightness: 80% + id: eyes_light + effect: "Snowfall" + - light.turn_on: + brightness: 80% + id: nose_light + effect: "Snowfall" + - light.turn_on: + brightness: 80% + id: hat_light + effect: "Snowfall" + - rtttl.play: + rtttl: !lambda 'return id(song_6).state;' + - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off - id: play_song_x then: @@ -1064,6 +1271,16 @@ script: - rtttl.play: rtttl: Sandstorm:d=16,o=6,b=85:e,e,e,e,8e,e,e,a,a,a,a,g,g,g,d,e,e,e,e, 8e,e,a,e,e,e,e,8e,e,d,e,e,e,e,e,8e,e,e,a,a,a,a,g,g,g,d,e,e,e,e,8e,e, a,e,e,e,e,8e,e,d,e,e,e,e,e,8e,e,e,a,a,a,a,g,g,g,d,e,e,e,e,8e,e,a,e, e,e,e,8e,e,d,e - lambda: "id(song_active) = id(rtttl_buzzer).is_playing();" + # Rtttl::play() returns early on a malformed string without ever + # reaching RUNNING, so on_finished_playback never fires and the lights + # above would stay on forever. Song Text is user-editable; one typo + # should not strand the ornament lit. + - if: + condition: + lambda: 'return !id(song_active);' + then: + - logger.log: "Song did not start - check the RTTTL string" + - script.execute: all_lights_off # First-boot indicator: every zone green at 30%, no timeout. Only # dismiss_first_boot clears it, so an un-acknowledged unit shows it again on diff --git a/Integrations/ESPHome/H-3.yaml b/Integrations/ESPHome/H-3.yaml index 43635c4..6d7f9bb 100644 --- a/Integrations/ESPHome/H-3.yaml +++ b/Integrations/ESPHome/H-3.yaml @@ -95,12 +95,33 @@ ota: esp32_ble_tracker: id: ble_tracker + # continuous is FALSE on purpose. The Bluetooth Proxy switch owns scanning, + # and the on_boot priority -300 handler restores it. With continuous: true + # the tracker starts a scan inside its own setup(), i.e. before that handler + # runs, so a fresh unit scanned for a moment even with the switch off - which + # is also the window esp32_improv needs to advertise on. scan_parameters: - continuous: true + continuous: false bluetooth_proxy: + id: ble_proxy + +# Improv over BLE. Required for the Made for ESPHome badge on any Wi-Fi +# project; improv_serial alone does not satisfy it. The component only +# advertises while the device has no Wi-Fi credentials (or has been off the +# network for wifi_timeout), and stops itself once wifi connects, so it costs +# nothing on a provisioned unit. Measured cost of adding it: +432 B RAM and +# +21 KB flash, because esp32_ble was already linked in for the proxy. +esp32_improv: + id: ble_improv + # 'none' = no physical authorization step, matching ESPHome's own reference + # ESP32-C6 factory config. Swap in a binary_sensor id here if the ornament + # should require a button press before it will accept credentials. + authorizer: none + status_indicator: improv_status_led http_request: + id: http_request_client 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 @@ -112,11 +133,15 @@ http_request: buffer_size_tx: 2048 safe_mode: + id: safe_mode_component update: - platform: http_request id: update_http_request name: Firmware Update + # Defaults to the empty device class, which renders as a generic update in + # Home Assistant. This one really is firmware. + device_class: firmware source: ${ota_stable_manifest} wifi: @@ -126,38 +151,78 @@ wifi: ssid: "Apollo H3 Hotspot" logger: + id: logger_component improv_serial: + id: serial_improv captive_portal: + id: captive_portal_component web_server: + id: web_server_component port: 80 version: 3 +# Drives the hat zone for esp32_improv. status_indicator must be a +# BinaryOutput, not a light, and it is the ONLY thing that sets +# improv::CAPABILITY_IDENTIFY - without it the provisioning app cannot ask the +# ornament to identify itself. Improv toggles this at most ~10 Hz and only on a +# real change, so it never floods the LED chain. +output: + - platform: template + type: binary + id: improv_status_led + write_action: + - if: + condition: + lambda: "return state;" + then: + - light.turn_on: + id: hat_light + brightness: 60% + red: 0% + green: 40% + blue: 100% + else: + - light.turn_off: hat_light + api: - services: - - service: play_buzzer + id: api_server + # No key on purpose. A bare 'encryption:' compiles in Noise support without + # baking a secret into a public config (Made for ESPHome forbids secrets and + # passwords in the published YAML), and ESPHome keeps the plaintext helper + # alongside it precisely so an adopting dashboard can hand the device a key + # over plaintext on first adoption. + encryption: + # 'actions' is the current spelling; ESPHome still rename_key's the old + # 'services' onto it, and the exposed Home Assistant service names are + # unchanged either way. + actions: + - action: play_buzzer variables: song_str: string then: - rtttl.play: rtttl: !lambda 'return song_str;' - - service: play_song_1 + - action: play_song_1 then: - script.execute: play_song_1 - - service: play_song_2 + - action: play_song_2 then: - script.execute: play_song_2 - - service: play_song_3 + - action: play_song_3 then: - script.execute: play_song_3 - - service: play_song_4 + - action: play_song_4 then: - script.execute: play_song_4 - - service: play_song_5 + - action: play_song_5 then: - script.execute: play_song_5 + - action: play_song_6 + then: + - script.execute: play_song_6 reboot_timeout: 0s on_client_connected: then: @@ -188,8 +253,12 @@ select: - script.execute: apply_ota_source button: + # Renamed from "Firmware Update": the update: entity above already carries + # that name, so Home Assistant showed the device two different entities with + # one label. This one force-installs the selected channel even when the + # update entity reports nothing available. - platform: template - name: "Firmware Update" + name: "Reinstall Firmware" id: update_firmware icon: mdi:cloud-download entity_category: "config" @@ -203,6 +272,18 @@ button: - delay: 5s - lambda: id(update_http_request).perform(true); + # Recovery entities. Both are diagnostic and both are things a support agent + # will ask for before suggesting a reflash. + - platform: restart + name: "Restart" + id: restart_button + entity_category: "diagnostic" + + - platform: safe_mode + name: "Restart (Safe Mode)" + id: safe_mode_button + entity_category: "diagnostic" + switch: - platform: template name: "Bluetooth Proxy" @@ -244,6 +325,10 @@ binary_sensor: - platform: homeassistant name: "OTA Mode" id: ota_mode + # Mirrors a Home Assistant helper back into HA, so it is never dashboard + # material. Kept as an entity rather than internal: true so existing + # automations that reference it do not break. + entity_category: "diagnostic" entity_id: input_boolean.apollo_ota_mode on_press: then: @@ -265,6 +350,37 @@ sensor: update_interval: 60s entity_category: "diagnostic" +# Support diagnostics. All disabled by default so they cost nothing on a +# normal install, but are one toggle away when someone reports a problem. +text_sensor: + - platform: wifi_info + ip_address: + name: "IP Address" + id: wifi_ip_address + entity_category: "diagnostic" + disabled_by_default: true + ssid: + name: "Connected SSID" + id: wifi_ssid + entity_category: "diagnostic" + disabled_by_default: true + bssid: + name: "Connected BSSID" + id: wifi_bssid + entity_category: "diagnostic" + disabled_by_default: true + mac_address: + name: "MAC Address" + id: wifi_mac_address + entity_category: "diagnostic" + disabled_by_default: true + + - platform: version + name: "ESPHome Version" + id: esphome_version + entity_category: "diagnostic" + disabled_by_default: true + script: - id: apply_ota_source # Sets the OTA manifest URL from the Firmware Channel select (Stable/Beta). diff --git a/README.md b/README.md index 5800e64..9a58f23 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - USB-C or battery powered ornament with 37 controllable RGB LEDs across five zones and a buzzer to play holiday tunes - Individually controllable body (24 LEDs), hat (5 LEDs), mouth (5 LEDs), eyes (2 LEDs), and nose (1 LED) -- Comes preloaded with 5 holiday tunes and RGB LED patterns +- Comes preloaded with 6 holiday tunes and RGB LED patterns - Comes flashed with basic firmware but can be flashed with our smart firmware to connect and control from Home Assistant - Could also be used as a bluetooth proxy or bluetooth tracker - Features an ESP32-C6 processor with 8MB flash diff --git a/static/_config.yml b/static/_config.yml index 9050ece..966a973 100644 --- a/static/_config.yml +++ b/static/_config.yml @@ -1,4 +1,6 @@ -# CHANGEME: Set these variable to your liking +# Jekyll config for the GitHub Pages installer site (static/index.html). +# index.html carries no front matter, so it is served verbatim and the theme +# below only applies to any future Markdown pages added here. title: Apollo H-3 Installer description: Powered by ESPHome and ESP Web Tools theme: jekyll-theme-slate