From 86d9aa200f289b21e77541a1cbfa0cae5c75ce9e Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:53:19 -0500 Subject: [PATCH 01/21] Add WizMote (ESP-NOW) remote control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pair a WizMote and control playback over ESP-NOW. Button mapping: ON play / OFF pause; Scene 1/2 previous/next track; Scene 3/4 fire HA events (cast1_wizmote_scene_3 / _4) for playlists/favorites; Bright +/- volume up/down; Night toggles the RGB light. Pairing via the WizMote MAC text entity, an Auto-Discovery switch, a status sensor, and a Clear Pairing button; the peer is restored on boot. ESP-NOW carries no channel in Core.yaml so it follows WiFi on CAST-1_W. CAST-1_ETH (no WiFi) adds a starting channel plus a scan interval that hops channels until the WizMote is heard, then holds. Bump version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/CAST-1_ETH.yaml | 26 +++ Integrations/ESPHome/Core.yaml | 279 ++++++++++++++++++++++++++- 2 files changed, 303 insertions(+), 2 deletions(-) diff --git a/Integrations/ESPHome/CAST-1_ETH.yaml b/Integrations/ESPHome/CAST-1_ETH.yaml index 7bc50cb..4ffb35c 100644 --- a/Integrations/ESPHome/CAST-1_ETH.yaml +++ b/Integrations/ESPHome/CAST-1_ETH.yaml @@ -92,5 +92,31 @@ text_sensor: id: eth_ip entity_category: "diagnostic" +# ESP-NOW has no WiFi to follow on the Ethernet variant, so it needs a +# starting channel. The interval below scans channels until the WizMote is +# heard, then holds. (The espnow handlers/entities live in Core.yaml.) +espnow: + channel: 1 + +interval: + - interval: 2s + then: + - if: + condition: + lambda: |- + std::string mac = id(wizmote_mac_address).state; + bool want = id(wizmote_discovery_mode).state || + (mac != "00:00:00:00:00:00" && mac != ""); + return want && (millis() - id(wizmote_last_packet_ms) > 30000); + then: + - lambda: |- + id(wizmote_scan_channel)++; + if (id(wizmote_scan_channel) > 13) id(wizmote_scan_channel) = 1; + - espnow.set_channel: + channel: !lambda 'return id(wizmote_scan_channel);' + - logger.log: + format: "WizMote channel scan: trying channel %d" + args: ['id(wizmote_scan_channel)'] + packages: core: !include Core.yaml diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index d83c4b2..1e95f53 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,17 @@ substitutions: - version: "26.6.18.1" + version: "26.6.18.2" + + # WizMote button codes (ESP-NOW) + WIZMOTE_BUTTON_ON: "1" + WIZMOTE_BUTTON_OFF: "2" + WIZMOTE_BUTTON_NIGHT: "3" + WIZMOTE_BUTTON_BRIGHT_UP: "9" + WIZMOTE_BUTTON_BRIGHT_DOWN: "8" + WIZMOTE_BUTTON_1: "16" + WIZMOTE_BUTTON_2: "17" + WIZMOTE_BUTTON_3: "18" + WIZMOTE_BUTTON_4: "19" + WIZMOTE_MAC_ADDRESS: "00:00:00:00:00:00" esp32: variant: ESP32S3 @@ -47,6 +59,23 @@ globals: - id: announcement_triggered type: bool initial_value: 'false' + # WizMote (ESP-NOW) + - id: wizmote_last_sequence + type: uint32_t + restore_value: no + initial_value: '0' + - id: wizmote_previous_mac + type: std::string + restore_value: no + initial_value: '""' + - id: wizmote_last_packet_ms + type: uint32_t + restore_value: no + initial_value: '0' + - id: wizmote_scan_channel + type: uint8_t + restore_value: no + initial_value: '1' psram: mode: octal @@ -74,6 +103,26 @@ button: - lambda: |- id(update_http_request).perform(true); + - platform: template + name: "Clear WizMote Pairing" + id: wizmote_clear_button + icon: "mdi:close-circle" + entity_category: config + on_press: + - lambda: |- + std::string mac = id(wizmote_mac_address).state; + if (mac != "00:00:00:00:00:00" && mac != "" && mac.length() == 17) { + std::array mac_bytes; + int parsed = sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + if (parsed == 6) { + id(espnow_component).del_peer(mac_bytes.data()); + } + } + id(wizmote_mac_address).publish_state("00:00:00:00:00:00"); + - lambda: 'id(wizmote_status).update();' + binary_sensor: - platform: status name: Online @@ -127,6 +176,17 @@ switch: name: Enable DAC id: enable_dac restore_mode: ALWAYS_ON + - platform: template + name: "WizMote Auto-Discovery" + id: wizmote_discovery_mode + icon: "mdi:radar" + entity_category: config + optimistic: true + restore_mode: ALWAYS_OFF + on_turn_on: + - lambda: 'id(wizmote_status).update();' + on_turn_off: + - lambda: 'id(wizmote_status).update();' text_sensor: - platform: sendspin @@ -152,6 +212,23 @@ text_sensor: update_interval: never entity_category: "diagnostic" + - platform: template + name: "WizMote Status" + id: wizmote_status + icon: "mdi:information" + entity_category: diagnostic + update_interval: 300s + lambda: |- + std::string mac = id(wizmote_mac_address).state; + bool discovery = id(wizmote_discovery_mode).state; + if (discovery) { + return std::string("Discovery mode active"); + } else if (mac == "00:00:00:00:00:00" || mac == "") { + return std::string("No WizMote paired"); + } else { + return std::string("Paired: " + mac); + } + light: - platform: esp32_rmt_led_strip id: rgb_light @@ -409,4 +486,202 @@ script: green: 0% blue: 0% - light.turn_off: - id: rgb_light \ No newline at end of file + id: rgb_light + + # WizMote button -> action mapping + - id: process_wizmote_button + parameters: + button: int + then: + # ON -> play / resume + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_ON};' + then: + - media_player.play: sendspin_group_media_player + # OFF -> pause + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' + then: + - media_player.pause: sendspin_group_media_player + # Scene 1 -> previous track + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_1};' + then: + - media_player.previous: sendspin_group_media_player + # Scene 2 -> next track + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_2};' + then: + - media_player.next: sendspin_group_media_player + # Scene 3 -> fire HA event (map to a playlist/favorite in Home Assistant) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_3};' + then: + - homeassistant.event: + event: esphome.cast1_wizmote_scene_3 + # Scene 4 -> fire HA event (map to a playlist/favorite in Home Assistant) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_4};' + then: + - homeassistant.event: + event: esphome.cast1_wizmote_scene_4 + # Bright + -> volume up (this speaker) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_UP};' + then: + - media_player.volume_up: external_media_player + # Bright - -> volume down (this speaker) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_DOWN};' + then: + - media_player.volume_down: external_media_player + # Night -> toggle the RGB light + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' + then: + - light.toggle: rgb_light + +# --------------------------------------------------------------------------- +# WizMote remote (ESP-NOW). The espnow: block carries no channel here so it +# follows the WiFi channel on CAST-1_W; CAST-1_ETH adds a channel + scan. +# --------------------------------------------------------------------------- +espnow: + id: espnow_component + auto_add_peer: false + enable_on_boot: true + on_unknown_peer: + then: + - lambda: |- + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", + info.src_addr[0], info.src_addr[1], info.src_addr[2], + info.src_addr[3], info.src_addr[4], info.src_addr[5]); + std::string sender_mac(mac_str); + + if (size != 13) return; + + uint8_t button = data[12]; + bool discovery_mode = id(wizmote_discovery_mode).state; + + if (discovery_mode) { + ESP_LOGI("wizmote", "Discovery: WizMote MAC: %s, Button: %d", sender_mac.c_str(), button); + std::string current_mac = id(wizmote_mac_address).state; + if (current_mac == "00:00:00:00:00:00" || current_mac == "" || current_mac != sender_mac) { + auto call = id(wizmote_mac_address).make_call(); + call.set_value(sender_mac); + call.perform(); + id(wizmote_discovery_mode).turn_off(); + } + return; + } + + ESP_LOGI("wizmote", "Unknown WizMote: %s (enable discovery mode to pair)", sender_mac.c_str()); + on_broadcast: + then: + - lambda: |- + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", + info.src_addr[0], info.src_addr[1], info.src_addr[2], + info.src_addr[3], info.src_addr[4], info.src_addr[5]); + std::string sender_mac(mac_str); + + if (size != 13) return; + + std::string configured_mac = id(wizmote_mac_address).state; + std::transform(configured_mac.begin(), configured_mac.end(), configured_mac.begin(), ::tolower); + + if (configured_mac == "00:00:00:00:00:00" || configured_mac == "" || sender_mac != configured_mac) return; + + // Note the last valid packet so the ETH channel-scan can lock on. + id(wizmote_last_packet_ms) = millis(); + + uint32_t sequence = (data[4] << 24) | (data[3] << 16) | (data[2] << 8) | data[1]; + uint8_t button = data[6]; + + if (sequence == id(wizmote_last_sequence)) return; + id(wizmote_last_sequence) = sequence; + + ESP_LOGI("wizmote", "Button: %d seq=%d from %s", button, sequence, sender_mac.c_str()); + id(process_wizmote_button).execute(button); + +text: + - platform: template + name: "WizMote MAC Address" + id: wizmote_mac_address + icon: "mdi:remote" + entity_category: config + initial_value: "${WIZMOTE_MAC_ADDRESS}" + optimistic: true + restore_value: true + mode: text + min_length: 17 + max_length: 17 + on_value: + - lambda: |- + std::string mac = x; + bool valid = true; + for (int i = 0; i < 17; i++) { + if (i % 3 == 2) { if (mac[i] != ':') { valid = false; break; } } + else { if (!std::isxdigit(mac[i])) { valid = false; break; } } + } + if (!valid) { + ESP_LOGW("wizmote", "Invalid MAC: %s", mac.c_str()); + return; + } + - if: + condition: + lambda: 'return (id(wizmote_previous_mac) != "" && id(wizmote_previous_mac) != "00:00:00:00:00:00");' + then: + - espnow.peer.delete: + address: !lambda |- + std::string mac = id(wizmote_previous_mac); + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; + - if: + condition: + lambda: 'return (x != "00:00:00:00:00:00" && x != "");' + then: + - espnow.peer.add: + address: !lambda |- + std::string mac = x; + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; + - lambda: 'id(wizmote_previous_mac) = x;' + - lambda: 'id(wizmote_status).update();' + +esphome: + on_boot: + # Re-add the saved WizMote peer after ESP-NOW is up. + - priority: -100 + then: + - delay: 2s + - if: + condition: + lambda: |- + std::string mac = id(wizmote_mac_address).state; + return (mac != "00:00:00:00:00:00" && mac != ""); + then: + - logger.log: "Restoring WizMote pairing on boot..." + - espnow.peer.add: + address: !lambda |- + std::string mac = id(wizmote_mac_address).state; + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; \ No newline at end of file From 29904615ad572162f93ed5e0d94e532866e3bafb Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:49:23 -0500 Subject: [PATCH 02/21] Log the action each WizMote button maps to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The on_broadcast handler logged the raw button number, but nothing showed what the dispatch actually did with it, so a press that lands on volume or an HA event looked like nothing happened. Log the mapped action in each branch (volume logs the resulting level), and warn on any unmapped button. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 1e95f53..8e194a6 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -498,30 +498,45 @@ script: condition: lambda: 'return button == ${WIZMOTE_BUTTON_ON};' then: + - logger.log: + tag: wizmote + format: "ON -> play" - media_player.play: sendspin_group_media_player # OFF -> pause - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' then: + - logger.log: + tag: wizmote + format: "OFF -> pause" - media_player.pause: sendspin_group_media_player # Scene 1 -> previous track - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_1};' then: + - logger.log: + tag: wizmote + format: "Scene 1 -> previous track" - media_player.previous: sendspin_group_media_player # Scene 2 -> next track - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_2};' then: + - logger.log: + tag: wizmote + format: "Scene 2 -> next track" - media_player.next: sendspin_group_media_player # Scene 3 -> fire HA event (map to a playlist/favorite in Home Assistant) - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_3};' then: + - logger.log: + tag: wizmote + format: "Scene 3 -> HA event esphome.cast1_wizmote_scene_3" - homeassistant.event: event: esphome.cast1_wizmote_scene_3 # Scene 4 -> fire HA event (map to a playlist/favorite in Home Assistant) @@ -529,6 +544,9 @@ script: condition: lambda: 'return button == ${WIZMOTE_BUTTON_4};' then: + - logger.log: + tag: wizmote + format: "Scene 4 -> HA event esphome.cast1_wizmote_scene_4" - homeassistant.event: event: esphome.cast1_wizmote_scene_4 # Bright + -> volume up (this speaker) @@ -537,18 +555,44 @@ script: lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_UP};' then: - media_player.volume_up: external_media_player + - logger.log: + tag: wizmote + format: "Bright+ -> volume up (now %.0f%%)" + args: ['id(external_media_player).volume * 100.0f'] # Bright - -> volume down (this speaker) - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_DOWN};' then: - media_player.volume_down: external_media_player + - logger.log: + tag: wizmote + format: "Bright- -> volume down (now %.0f%%)" + args: ['id(external_media_player).volume * 100.0f'] # Night -> toggle the RGB light - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' then: + - logger.log: + tag: wizmote + format: "Night -> toggle RGB light" - light.toggle: rgb_light + # Anything else: log it so unmapped buttons are visible + - if: + condition: + lambda: |- + return button != ${WIZMOTE_BUTTON_ON} && button != ${WIZMOTE_BUTTON_OFF} && + button != ${WIZMOTE_BUTTON_NIGHT} && button != ${WIZMOTE_BUTTON_BRIGHT_UP} && + button != ${WIZMOTE_BUTTON_BRIGHT_DOWN} && button != ${WIZMOTE_BUTTON_1} && + button != ${WIZMOTE_BUTTON_2} && button != ${WIZMOTE_BUTTON_3} && + button != ${WIZMOTE_BUTTON_4}; + then: + - logger.log: + tag: wizmote + level: WARN + format: "unmapped button %d" + args: ['button'] # --------------------------------------------------------------------------- # WizMote remote (ESP-NOW). The espnow: block carries no channel here so it From cdb3f05d7d9938ae531d980a494b4f6330d33ca5 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:25:06 -0500 Subject: [PATCH 03/21] Bump version to 26.6.19.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 8e194a6..f961e74 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.6.18.2" + version: "26.6.19.1" # WizMote button codes (ESP-NOW) WIZMOTE_BUTTON_ON: "1" From d2247bedb206ba824b81f543f54e4be029453096 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:45:40 -0500 Subject: [PATCH 04/21] Move WizMote config into wizmote.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull the WizMote pieces (espnow, pairing entities, button dispatch, globals, substitutions, boot restore) out of Core.yaml into their own wizmote.yaml, included via packages. Pure reorg: the fully-resolved config is byte-for-byte the same set of entities on both variants. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 320 +--------------------------- Integrations/ESPHome/wizmote.yaml | 332 ++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 318 deletions(-) create mode 100644 Integrations/ESPHome/wizmote.yaml diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index f961e74..dc1b77f 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,17 +1,8 @@ substitutions: version: "26.6.19.1" - # WizMote button codes (ESP-NOW) - WIZMOTE_BUTTON_ON: "1" - WIZMOTE_BUTTON_OFF: "2" - WIZMOTE_BUTTON_NIGHT: "3" - WIZMOTE_BUTTON_BRIGHT_UP: "9" - WIZMOTE_BUTTON_BRIGHT_DOWN: "8" - WIZMOTE_BUTTON_1: "16" - WIZMOTE_BUTTON_2: "17" - WIZMOTE_BUTTON_3: "18" - WIZMOTE_BUTTON_4: "19" - WIZMOTE_MAC_ADDRESS: "00:00:00:00:00:00" +packages: + wizmote: !include wizmote.yaml esp32: variant: ESP32S3 @@ -59,23 +50,6 @@ globals: - id: announcement_triggered type: bool initial_value: 'false' - # WizMote (ESP-NOW) - - id: wizmote_last_sequence - type: uint32_t - restore_value: no - initial_value: '0' - - id: wizmote_previous_mac - type: std::string - restore_value: no - initial_value: '""' - - id: wizmote_last_packet_ms - type: uint32_t - restore_value: no - initial_value: '0' - - id: wizmote_scan_channel - type: uint8_t - restore_value: no - initial_value: '1' psram: mode: octal @@ -103,26 +77,6 @@ button: - lambda: |- id(update_http_request).perform(true); - - platform: template - name: "Clear WizMote Pairing" - id: wizmote_clear_button - icon: "mdi:close-circle" - entity_category: config - on_press: - - lambda: |- - std::string mac = id(wizmote_mac_address).state; - if (mac != "00:00:00:00:00:00" && mac != "" && mac.length() == 17) { - std::array mac_bytes; - int parsed = sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], - &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); - if (parsed == 6) { - id(espnow_component).del_peer(mac_bytes.data()); - } - } - id(wizmote_mac_address).publish_state("00:00:00:00:00:00"); - - lambda: 'id(wizmote_status).update();' - binary_sensor: - platform: status name: Online @@ -176,17 +130,6 @@ switch: name: Enable DAC id: enable_dac restore_mode: ALWAYS_ON - - platform: template - name: "WizMote Auto-Discovery" - id: wizmote_discovery_mode - icon: "mdi:radar" - entity_category: config - optimistic: true - restore_mode: ALWAYS_OFF - on_turn_on: - - lambda: 'id(wizmote_status).update();' - on_turn_off: - - lambda: 'id(wizmote_status).update();' text_sensor: - platform: sendspin @@ -212,23 +155,6 @@ text_sensor: update_interval: never entity_category: "diagnostic" - - platform: template - name: "WizMote Status" - id: wizmote_status - icon: "mdi:information" - entity_category: diagnostic - update_interval: 300s - lambda: |- - std::string mac = id(wizmote_mac_address).state; - bool discovery = id(wizmote_discovery_mode).state; - if (discovery) { - return std::string("Discovery mode active"); - } else if (mac == "00:00:00:00:00:00" || mac == "") { - return std::string("No WizMote paired"); - } else { - return std::string("Paired: " + mac); - } - light: - platform: esp32_rmt_led_strip id: rgb_light @@ -487,245 +413,3 @@ script: blue: 0% - light.turn_off: id: rgb_light - - # WizMote button -> action mapping - - id: process_wizmote_button - parameters: - button: int - then: - # ON -> play / resume - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_ON};' - then: - - logger.log: - tag: wizmote - format: "ON -> play" - - media_player.play: sendspin_group_media_player - # OFF -> pause - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' - then: - - logger.log: - tag: wizmote - format: "OFF -> pause" - - media_player.pause: sendspin_group_media_player - # Scene 1 -> previous track - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_1};' - then: - - logger.log: - tag: wizmote - format: "Scene 1 -> previous track" - - media_player.previous: sendspin_group_media_player - # Scene 2 -> next track - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_2};' - then: - - logger.log: - tag: wizmote - format: "Scene 2 -> next track" - - media_player.next: sendspin_group_media_player - # Scene 3 -> fire HA event (map to a playlist/favorite in Home Assistant) - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_3};' - then: - - logger.log: - tag: wizmote - format: "Scene 3 -> HA event esphome.cast1_wizmote_scene_3" - - homeassistant.event: - event: esphome.cast1_wizmote_scene_3 - # Scene 4 -> fire HA event (map to a playlist/favorite in Home Assistant) - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_4};' - then: - - logger.log: - tag: wizmote - format: "Scene 4 -> HA event esphome.cast1_wizmote_scene_4" - - homeassistant.event: - event: esphome.cast1_wizmote_scene_4 - # Bright + -> volume up (this speaker) - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_UP};' - then: - - media_player.volume_up: external_media_player - - logger.log: - tag: wizmote - format: "Bright+ -> volume up (now %.0f%%)" - args: ['id(external_media_player).volume * 100.0f'] - # Bright - -> volume down (this speaker) - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_DOWN};' - then: - - media_player.volume_down: external_media_player - - logger.log: - tag: wizmote - format: "Bright- -> volume down (now %.0f%%)" - args: ['id(external_media_player).volume * 100.0f'] - # Night -> toggle the RGB light - - if: - condition: - lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' - then: - - logger.log: - tag: wizmote - format: "Night -> toggle RGB light" - - light.toggle: rgb_light - # Anything else: log it so unmapped buttons are visible - - if: - condition: - lambda: |- - return button != ${WIZMOTE_BUTTON_ON} && button != ${WIZMOTE_BUTTON_OFF} && - button != ${WIZMOTE_BUTTON_NIGHT} && button != ${WIZMOTE_BUTTON_BRIGHT_UP} && - button != ${WIZMOTE_BUTTON_BRIGHT_DOWN} && button != ${WIZMOTE_BUTTON_1} && - button != ${WIZMOTE_BUTTON_2} && button != ${WIZMOTE_BUTTON_3} && - button != ${WIZMOTE_BUTTON_4}; - then: - - logger.log: - tag: wizmote - level: WARN - format: "unmapped button %d" - args: ['button'] - -# --------------------------------------------------------------------------- -# WizMote remote (ESP-NOW). The espnow: block carries no channel here so it -# follows the WiFi channel on CAST-1_W; CAST-1_ETH adds a channel + scan. -# --------------------------------------------------------------------------- -espnow: - id: espnow_component - auto_add_peer: false - enable_on_boot: true - on_unknown_peer: - then: - - lambda: |- - char mac_str[18]; - snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", - info.src_addr[0], info.src_addr[1], info.src_addr[2], - info.src_addr[3], info.src_addr[4], info.src_addr[5]); - std::string sender_mac(mac_str); - - if (size != 13) return; - - uint8_t button = data[12]; - bool discovery_mode = id(wizmote_discovery_mode).state; - - if (discovery_mode) { - ESP_LOGI("wizmote", "Discovery: WizMote MAC: %s, Button: %d", sender_mac.c_str(), button); - std::string current_mac = id(wizmote_mac_address).state; - if (current_mac == "00:00:00:00:00:00" || current_mac == "" || current_mac != sender_mac) { - auto call = id(wizmote_mac_address).make_call(); - call.set_value(sender_mac); - call.perform(); - id(wizmote_discovery_mode).turn_off(); - } - return; - } - - ESP_LOGI("wizmote", "Unknown WizMote: %s (enable discovery mode to pair)", sender_mac.c_str()); - on_broadcast: - then: - - lambda: |- - char mac_str[18]; - snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", - info.src_addr[0], info.src_addr[1], info.src_addr[2], - info.src_addr[3], info.src_addr[4], info.src_addr[5]); - std::string sender_mac(mac_str); - - if (size != 13) return; - - std::string configured_mac = id(wizmote_mac_address).state; - std::transform(configured_mac.begin(), configured_mac.end(), configured_mac.begin(), ::tolower); - - if (configured_mac == "00:00:00:00:00:00" || configured_mac == "" || sender_mac != configured_mac) return; - - // Note the last valid packet so the ETH channel-scan can lock on. - id(wizmote_last_packet_ms) = millis(); - - uint32_t sequence = (data[4] << 24) | (data[3] << 16) | (data[2] << 8) | data[1]; - uint8_t button = data[6]; - - if (sequence == id(wizmote_last_sequence)) return; - id(wizmote_last_sequence) = sequence; - - ESP_LOGI("wizmote", "Button: %d seq=%d from %s", button, sequence, sender_mac.c_str()); - id(process_wizmote_button).execute(button); - -text: - - platform: template - name: "WizMote MAC Address" - id: wizmote_mac_address - icon: "mdi:remote" - entity_category: config - initial_value: "${WIZMOTE_MAC_ADDRESS}" - optimistic: true - restore_value: true - mode: text - min_length: 17 - max_length: 17 - on_value: - - lambda: |- - std::string mac = x; - bool valid = true; - for (int i = 0; i < 17; i++) { - if (i % 3 == 2) { if (mac[i] != ':') { valid = false; break; } } - else { if (!std::isxdigit(mac[i])) { valid = false; break; } } - } - if (!valid) { - ESP_LOGW("wizmote", "Invalid MAC: %s", mac.c_str()); - return; - } - - if: - condition: - lambda: 'return (id(wizmote_previous_mac) != "" && id(wizmote_previous_mac) != "00:00:00:00:00:00");' - then: - - espnow.peer.delete: - address: !lambda |- - std::string mac = id(wizmote_previous_mac); - std::array mac_bytes; - sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], - &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); - return mac_bytes; - - if: - condition: - lambda: 'return (x != "00:00:00:00:00:00" && x != "");' - then: - - espnow.peer.add: - address: !lambda |- - std::string mac = x; - std::array mac_bytes; - sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], - &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); - return mac_bytes; - - lambda: 'id(wizmote_previous_mac) = x;' - - lambda: 'id(wizmote_status).update();' - -esphome: - on_boot: - # Re-add the saved WizMote peer after ESP-NOW is up. - - priority: -100 - then: - - delay: 2s - - if: - condition: - lambda: |- - std::string mac = id(wizmote_mac_address).state; - return (mac != "00:00:00:00:00:00" && mac != ""); - then: - - logger.log: "Restoring WizMote pairing on boot..." - - espnow.peer.add: - address: !lambda |- - std::string mac = id(wizmote_mac_address).state; - std::array mac_bytes; - sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], - &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); - return mac_bytes; \ No newline at end of file diff --git a/Integrations/ESPHome/wizmote.yaml b/Integrations/ESPHome/wizmote.yaml new file mode 100644 index 0000000..f7ee212 --- /dev/null +++ b/Integrations/ESPHome/wizmote.yaml @@ -0,0 +1,332 @@ +# --------------------------------------------------------------------------- +# WizMote remote control (ESP-NOW). Included by Core.yaml. +# +# Pair a WizMote with the Auto-Discovery switch, then control playback: +# ON/OFF -> play/pause, Scene 1/2 -> previous/next, Scene 3/4 -> HA events +# (esphome.cast1_wizmote_scene_3 / _4), Bright +/- -> volume, Night -> RGB light. +# +# The espnow: block carries no channel here so it follows the WiFi channel on +# CAST-1_W. CAST-1_ETH (no WiFi) adds a starting channel + a scan interval. +# --------------------------------------------------------------------------- + +substitutions: + # WizMote button codes (ESP-NOW) + WIZMOTE_BUTTON_ON: "1" + WIZMOTE_BUTTON_OFF: "2" + WIZMOTE_BUTTON_NIGHT: "3" + WIZMOTE_BUTTON_BRIGHT_UP: "9" + WIZMOTE_BUTTON_BRIGHT_DOWN: "8" + WIZMOTE_BUTTON_1: "16" + WIZMOTE_BUTTON_2: "17" + WIZMOTE_BUTTON_3: "18" + WIZMOTE_BUTTON_4: "19" + WIZMOTE_MAC_ADDRESS: "00:00:00:00:00:00" + +globals: + - id: wizmote_last_sequence + type: uint32_t + restore_value: no + initial_value: '0' + - id: wizmote_previous_mac + type: std::string + restore_value: no + initial_value: '""' + - id: wizmote_last_packet_ms + type: uint32_t + restore_value: no + initial_value: '0' + - id: wizmote_scan_channel + type: uint8_t + restore_value: no + initial_value: '1' + +button: + - platform: template + name: "Clear WizMote Pairing" + id: wizmote_clear_button + icon: "mdi:close-circle" + entity_category: config + on_press: + - lambda: |- + std::string mac = id(wizmote_mac_address).state; + if (mac != "00:00:00:00:00:00" && mac != "" && mac.length() == 17) { + std::array mac_bytes; + int parsed = sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + if (parsed == 6) { + id(espnow_component).del_peer(mac_bytes.data()); + } + } + id(wizmote_mac_address).publish_state("00:00:00:00:00:00"); + - lambda: 'id(wizmote_status).update();' + +switch: + - platform: template + name: "WizMote Auto-Discovery" + id: wizmote_discovery_mode + icon: "mdi:radar" + entity_category: config + optimistic: true + restore_mode: ALWAYS_OFF + on_turn_on: + - lambda: 'id(wizmote_status).update();' + on_turn_off: + - lambda: 'id(wizmote_status).update();' + +text_sensor: + - platform: template + name: "WizMote Status" + id: wizmote_status + icon: "mdi:information" + entity_category: diagnostic + update_interval: 300s + lambda: |- + std::string mac = id(wizmote_mac_address).state; + bool discovery = id(wizmote_discovery_mode).state; + if (discovery) { + return std::string("Discovery mode active"); + } else if (mac == "00:00:00:00:00:00" || mac == "") { + return std::string("No WizMote paired"); + } else { + return std::string("Paired: " + mac); + } + +text: + - platform: template + name: "WizMote MAC Address" + id: wizmote_mac_address + icon: "mdi:remote" + entity_category: config + initial_value: "${WIZMOTE_MAC_ADDRESS}" + optimistic: true + restore_value: true + mode: text + min_length: 17 + max_length: 17 + on_value: + - lambda: |- + std::string mac = x; + bool valid = true; + for (int i = 0; i < 17; i++) { + if (i % 3 == 2) { if (mac[i] != ':') { valid = false; break; } } + else { if (!std::isxdigit(mac[i])) { valid = false; break; } } + } + if (!valid) { + ESP_LOGW("wizmote", "Invalid MAC: %s", mac.c_str()); + return; + } + - if: + condition: + lambda: 'return (id(wizmote_previous_mac) != "" && id(wizmote_previous_mac) != "00:00:00:00:00:00");' + then: + - espnow.peer.delete: + address: !lambda |- + std::string mac = id(wizmote_previous_mac); + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; + - if: + condition: + lambda: 'return (x != "00:00:00:00:00:00" && x != "");' + then: + - espnow.peer.add: + address: !lambda |- + std::string mac = x; + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; + - lambda: 'id(wizmote_previous_mac) = x;' + - lambda: 'id(wizmote_status).update();' + +script: + # WizMote button -> action mapping + - id: process_wizmote_button + parameters: + button: int + then: + # ON -> play / resume + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_ON};' + then: + - logger.log: + tag: wizmote + format: "ON -> play" + - media_player.play: sendspin_group_media_player + # OFF -> pause + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' + then: + - logger.log: + tag: wizmote + format: "OFF -> pause" + - media_player.pause: sendspin_group_media_player + # Scene 1 -> previous track + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_1};' + then: + - logger.log: + tag: wizmote + format: "Scene 1 -> previous track" + - media_player.previous: sendspin_group_media_player + # Scene 2 -> next track + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_2};' + then: + - logger.log: + tag: wizmote + format: "Scene 2 -> next track" + - media_player.next: sendspin_group_media_player + # Scene 3 -> fire HA event (map to a playlist/favorite in Home Assistant) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_3};' + then: + - logger.log: + tag: wizmote + format: "Scene 3 -> HA event esphome.cast1_wizmote_scene_3" + - homeassistant.event: + event: esphome.cast1_wizmote_scene_3 + # Scene 4 -> fire HA event (map to a playlist/favorite in Home Assistant) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_4};' + then: + - logger.log: + tag: wizmote + format: "Scene 4 -> HA event esphome.cast1_wizmote_scene_4" + - homeassistant.event: + event: esphome.cast1_wizmote_scene_4 + # Bright + -> volume up (this speaker) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_UP};' + then: + - media_player.volume_up: external_media_player + - logger.log: + tag: wizmote + format: "Bright+ -> volume up (now %.0f%%)" + args: ['id(external_media_player).volume * 100.0f'] + # Bright - -> volume down (this speaker) + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_DOWN};' + then: + - media_player.volume_down: external_media_player + - logger.log: + tag: wizmote + format: "Bright- -> volume down (now %.0f%%)" + args: ['id(external_media_player).volume * 100.0f'] + # Night -> toggle the RGB light + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' + then: + - logger.log: + tag: wizmote + format: "Night -> toggle RGB light" + - light.toggle: rgb_light + # Anything else: log it so unmapped buttons are visible + - if: + condition: + lambda: |- + return button != ${WIZMOTE_BUTTON_ON} && button != ${WIZMOTE_BUTTON_OFF} && + button != ${WIZMOTE_BUTTON_NIGHT} && button != ${WIZMOTE_BUTTON_BRIGHT_UP} && + button != ${WIZMOTE_BUTTON_BRIGHT_DOWN} && button != ${WIZMOTE_BUTTON_1} && + button != ${WIZMOTE_BUTTON_2} && button != ${WIZMOTE_BUTTON_3} && + button != ${WIZMOTE_BUTTON_4}; + then: + - logger.log: + tag: wizmote + level: WARN + format: "unmapped button %d" + args: ['button'] + +espnow: + id: espnow_component + auto_add_peer: false + enable_on_boot: true + on_unknown_peer: + then: + - lambda: |- + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", + info.src_addr[0], info.src_addr[1], info.src_addr[2], + info.src_addr[3], info.src_addr[4], info.src_addr[5]); + std::string sender_mac(mac_str); + + if (size != 13) return; + + uint8_t button = data[12]; + bool discovery_mode = id(wizmote_discovery_mode).state; + + if (discovery_mode) { + ESP_LOGI("wizmote", "Discovery: WizMote MAC: %s, Button: %d", sender_mac.c_str(), button); + std::string current_mac = id(wizmote_mac_address).state; + if (current_mac == "00:00:00:00:00:00" || current_mac == "" || current_mac != sender_mac) { + auto call = id(wizmote_mac_address).make_call(); + call.set_value(sender_mac); + call.perform(); + id(wizmote_discovery_mode).turn_off(); + } + return; + } + + ESP_LOGI("wizmote", "Unknown WizMote: %s (enable discovery mode to pair)", sender_mac.c_str()); + on_broadcast: + then: + - lambda: |- + char mac_str[18]; + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", + info.src_addr[0], info.src_addr[1], info.src_addr[2], + info.src_addr[3], info.src_addr[4], info.src_addr[5]); + std::string sender_mac(mac_str); + + if (size != 13) return; + + std::string configured_mac = id(wizmote_mac_address).state; + std::transform(configured_mac.begin(), configured_mac.end(), configured_mac.begin(), ::tolower); + + if (configured_mac == "00:00:00:00:00:00" || configured_mac == "" || sender_mac != configured_mac) return; + + // Note the last valid packet so the ETH channel-scan can lock on. + id(wizmote_last_packet_ms) = millis(); + + uint32_t sequence = (data[4] << 24) | (data[3] << 16) | (data[2] << 8) | data[1]; + uint8_t button = data[6]; + + if (sequence == id(wizmote_last_sequence)) return; + id(wizmote_last_sequence) = sequence; + + ESP_LOGI("wizmote", "Button: %d seq=%d from %s", button, sequence, sender_mac.c_str()); + id(process_wizmote_button).execute(button); + +esphome: + on_boot: + # Re-add the saved WizMote peer after ESP-NOW is up. + - priority: -100 + then: + - delay: 2s + - if: + condition: + lambda: |- + std::string mac = id(wizmote_mac_address).state; + return (mac != "00:00:00:00:00:00" && mac != ""); + then: + - logger.log: "Restoring WizMote pairing on boot..." + - espnow.peer.add: + address: !lambda |- + std::string mac = id(wizmote_mac_address).state; + std::array mac_bytes; + sscanf(mac.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_bytes[0], &mac_bytes[1], &mac_bytes[2], + &mac_bytes[3], &mac_bytes[4], &mac_bytes[5]); + return mac_bytes; From 2789a7d6fd46fbf92e3e55034abf9b88a81a76f3 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:03:04 -0500 Subject: [PATCH 05/21] Make WizMote buttons remappable from Home Assistant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a select per WizMote button (On, Off, Night, Brightness Up/Down, Button 1-4) so the action is chosen in HA, no YAML editing. Options: Nothing / Play / Pause / Play-Pause / Next / Previous / Volume Up/Down / Toggle Light / Send HA Event. Defaults match the previous fixed mapping. process_wizmote_button now looks up the pressed button's select and runs a shared run_wizmote_action sub-script. "Send HA Event" fires esphome.cast1_wizmote_event with the button label in the payload, so any button can be wired to a playlist/scene/etc. in a HA automation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 2 +- Integrations/ESPHome/wizmote.yaml | 254 ++++++++++++++++++++++++------ 2 files changed, 203 insertions(+), 53 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index dc1b77f..3649d50 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.6.19.1" + version: "26.6.19.2" packages: wizmote: !include wizmote.yaml diff --git a/Integrations/ESPHome/wizmote.yaml b/Integrations/ESPHome/wizmote.yaml index f7ee212..063d3b1 100644 --- a/Integrations/ESPHome/wizmote.yaml +++ b/Integrations/ESPHome/wizmote.yaml @@ -74,6 +74,102 @@ switch: on_turn_off: - lambda: 'id(wizmote_status).update();' +# One dropdown per WizMote button. Change these in Home Assistant to remap a +# button -- no YAML editing. "Send HA Event" fires esphome.cast1_wizmote_event +# with the button label in the payload so you can wire it to anything in HA. +select: + - platform: template + name: "WizMote On" + id: wizmote_action_on + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Play" + options: &wizmote_actions + - "Nothing" + - "Play" + - "Pause" + - "Play / Pause" + - "Next Track" + - "Previous Track" + - "Volume Up" + - "Volume Down" + - "Toggle Light" + - "Send HA Event" + - platform: template + name: "WizMote Off" + id: wizmote_action_off + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Pause" + options: *wizmote_actions + - platform: template + name: "WizMote Night" + id: wizmote_action_night + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Toggle Light" + options: *wizmote_actions + - platform: template + name: "WizMote Brightness Up" + id: wizmote_action_bright_up + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Volume Up" + options: *wizmote_actions + - platform: template + name: "WizMote Brightness Down" + id: wizmote_action_bright_down + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Volume Down" + options: *wizmote_actions + - platform: template + name: "WizMote Button 1" + id: wizmote_action_btn1 + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Previous Track" + options: *wizmote_actions + - platform: template + name: "WizMote Button 2" + id: wizmote_action_btn2 + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Next Track" + options: *wizmote_actions + - platform: template + name: "WizMote Button 3" + id: wizmote_action_btn3 + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Send HA Event" + options: *wizmote_actions + - platform: template + name: "WizMote Button 4" + id: wizmote_action_btn4 + icon: "mdi:gesture-tap-button" + entity_category: config + optimistic: true + restore_value: true + initial_option: "Send HA Event" + options: *wizmote_actions + text_sensor: - platform: template name: "WizMote Status" @@ -144,97 +240,151 @@ text: - lambda: 'id(wizmote_status).update();' script: - # WizMote button -> action mapping - - id: process_wizmote_button + # Run one action. `action` is the chosen menu string; `button` is the label + # used in the HA event payload when the action is "Send HA Event". + - id: run_wizmote_action parameters: - button: int + action: string + button: string then: - # ON -> play / resume + - logger.log: + tag: wizmote + format: "button %s -> %s" + args: ['button.c_str()', 'action.c_str()'] - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_ON};' + lambda: 'return action == "Play";' then: - - logger.log: - tag: wizmote - format: "ON -> play" - media_player.play: sendspin_group_media_player - # OFF -> pause - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' + lambda: 'return action == "Pause";' then: - - logger.log: - tag: wizmote - format: "OFF -> pause" - media_player.pause: sendspin_group_media_player - # Scene 1 -> previous track - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_1};' + lambda: 'return action == "Play / Pause";' then: - - logger.log: - tag: wizmote - format: "Scene 1 -> previous track" - - media_player.previous: sendspin_group_media_player - # Scene 2 -> next track + - media_player.toggle: sendspin_group_media_player - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_2};' + lambda: 'return action == "Next Track";' then: - - logger.log: - tag: wizmote - format: "Scene 2 -> next track" - media_player.next: sendspin_group_media_player - # Scene 3 -> fire HA event (map to a playlist/favorite in Home Assistant) - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_3};' + lambda: 'return action == "Previous Track";' + then: + - media_player.previous: sendspin_group_media_player + - if: + condition: + lambda: 'return action == "Volume Up";' then: + - media_player.volume_up: external_media_player - logger.log: tag: wizmote - format: "Scene 3 -> HA event esphome.cast1_wizmote_scene_3" - - homeassistant.event: - event: esphome.cast1_wizmote_scene_3 - # Scene 4 -> fire HA event (map to a playlist/favorite in Home Assistant) + format: " volume now %.0f%%" + args: ['id(external_media_player).volume * 100.0f'] - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_4};' + lambda: 'return action == "Volume Down";' then: + - media_player.volume_down: external_media_player - logger.log: tag: wizmote - format: "Scene 4 -> HA event esphome.cast1_wizmote_scene_4" + format: " volume now %.0f%%" + args: ['id(external_media_player).volume * 100.0f'] + - if: + condition: + lambda: 'return action == "Toggle Light";' + then: + - light.toggle: rgb_light + - if: + condition: + lambda: 'return action == "Send HA Event";' + then: - homeassistant.event: - event: esphome.cast1_wizmote_scene_4 - # Bright + -> volume up (this speaker) + event: esphome.cast1_wizmote_event + data: + button: !lambda 'return button;' + + # Look up the configured action for the pressed button, then run it. + - id: process_wizmote_button + parameters: + button: int + then: + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_ON};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_on).state;' + button: "on" + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_OFF};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_off).state;' + button: "off" + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_night).state;' + button: "night" - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_UP};' then: - - media_player.volume_up: external_media_player - - logger.log: - tag: wizmote - format: "Bright+ -> volume up (now %.0f%%)" - args: ['id(external_media_player).volume * 100.0f'] - # Bright - -> volume down (this speaker) + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_bright_up).state;' + button: "brightness_up" - if: condition: lambda: 'return button == ${WIZMOTE_BUTTON_BRIGHT_DOWN};' then: - - media_player.volume_down: external_media_player - - logger.log: - tag: wizmote - format: "Bright- -> volume down (now %.0f%%)" - args: ['id(external_media_player).volume * 100.0f'] - # Night -> toggle the RGB light + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_bright_down).state;' + button: "brightness_down" - if: condition: - lambda: 'return button == ${WIZMOTE_BUTTON_NIGHT};' + lambda: 'return button == ${WIZMOTE_BUTTON_1};' then: - - logger.log: - tag: wizmote - format: "Night -> toggle RGB light" - - light.toggle: rgb_light - # Anything else: log it so unmapped buttons are visible + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_btn1).state;' + button: "1" + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_2};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_btn2).state;' + button: "2" + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_3};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_btn3).state;' + button: "3" + - if: + condition: + lambda: 'return button == ${WIZMOTE_BUTTON_4};' + then: + - script.execute: + id: run_wizmote_action + action: !lambda 'return id(wizmote_action_btn4).state;' + button: "4" - if: condition: lambda: |- From 041030d50882d6c5234b5a4db3a739f712831d93 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:24:47 -0500 Subject: [PATCH 06/21] Add Home Assistant blueprint for WizMote buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets users wire any "Send HA Event" button to an HA action (playlist, scene, script) without editing automations by hand. One action selector per button; triggers on esphome.cast1_wizmote_event. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../HomeAssistant/cast-1-wizmote.yaml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Integrations/HomeAssistant/cast-1-wizmote.yaml diff --git a/Integrations/HomeAssistant/cast-1-wizmote.yaml b/Integrations/HomeAssistant/cast-1-wizmote.yaml new file mode 100644 index 0000000..696f176 --- /dev/null +++ b/Integrations/HomeAssistant/cast-1-wizmote.yaml @@ -0,0 +1,88 @@ +blueprint: + name: Apollo CAST-1 WizMote + description: > + Run Home Assistant actions from an Apollo CAST-1 WizMote. + + On the CAST-1, set any button's dropdown to "Send HA Event" (Button 3 and 4 + ship that way). Then choose what each button does here. Buttons left on a + built-in action (Play, Volume, etc.) are handled on the device and never + reach this automation. + domain: automation + source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Integrations/HomeAssistant/cast-1-wizmote.yaml + input: + on_action: + name: On + default: [] + selector: + action: + off_action: + name: Off + default: [] + selector: + action: + night_action: + name: Night + default: [] + selector: + action: + bright_up_action: + name: Brightness Up + default: [] + selector: + action: + bright_down_action: + name: Brightness Down + default: [] + selector: + action: + button_1_action: + name: Button 1 + default: [] + selector: + action: + button_2_action: + name: Button 2 + default: [] + selector: + action: + button_3_action: + name: Button 3 + default: [] + selector: + action: + button_4_action: + name: Button 4 + default: [] + selector: + action: + +mode: queued +max: 10 + +trigger: + - platform: event + event_type: esphome.cast1_wizmote_event + +variables: + pressed: "{{ trigger.event.data.button }}" + +action: + - choose: + - conditions: "{{ pressed == 'on' }}" + sequence: !input on_action + - conditions: "{{ pressed == 'off' }}" + sequence: !input off_action + - conditions: "{{ pressed == 'night' }}" + sequence: !input night_action + - conditions: "{{ pressed == 'brightness_up' }}" + sequence: !input bright_up_action + - conditions: "{{ pressed == 'brightness_down' }}" + sequence: !input bright_down_action + - conditions: "{{ pressed == '1' }}" + sequence: !input button_1_action + - conditions: "{{ pressed == '2' }}" + sequence: !input button_2_action + - conditions: "{{ pressed == '3' }}" + sequence: !input button_3_action + - conditions: "{{ pressed == '4' }}" + sequence: !input button_4_action From 266716c2f5fc64d3a91164984bfa6d416eb99c24 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:30:17 -0500 Subject: [PATCH 07/21] Move WizMote blueprint to Blueprints/ and fix On/Off names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit YAML parsed the bare On/Off input names as booleans, so HA rejected the import. Quote all names. Also relocate the blueprint to a top-level Blueprints/ directory and point source_url at the new path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cast-1-wizmote.yaml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) rename {Integrations/HomeAssistant => Blueprints}/cast-1-wizmote.yaml (89%) diff --git a/Integrations/HomeAssistant/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml similarity index 89% rename from Integrations/HomeAssistant/cast-1-wizmote.yaml rename to Blueprints/cast-1-wizmote.yaml index 696f176..c9dc5e4 100644 --- a/Integrations/HomeAssistant/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -8,50 +8,50 @@ blueprint: built-in action (Play, Volume, etc.) are handled on the device and never reach this automation. domain: automation - source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Integrations/HomeAssistant/cast-1-wizmote.yaml + source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/cast-1-wizmote.yaml input: on_action: - name: On + name: "On" default: [] selector: action: off_action: - name: Off + name: "Off" default: [] selector: action: night_action: - name: Night + name: "Night" default: [] selector: action: bright_up_action: - name: Brightness Up + name: "Brightness Up" default: [] selector: action: bright_down_action: - name: Brightness Down + name: "Brightness Down" default: [] selector: action: button_1_action: - name: Button 1 + name: "Button 1" default: [] selector: action: button_2_action: - name: Button 2 + name: "Button 2" default: [] selector: action: button_3_action: - name: Button 3 + name: "Button 3" default: [] selector: action: button_4_action: - name: Button 4 + name: "Button 4" default: [] selector: action: From 0e21dda0691735feb89f326d524f3866a83e1623 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:37:34 -0500 Subject: [PATCH 08/21] Bump version to 26.6.19.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So testers can confirm they flashed this build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 3649d50..b2e2428 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.6.19.2" + version: "26.6.19.3" packages: wizmote: !include wizmote.yaml From 1d977cb38fffbb2d671da8fba826995c24fe55c3 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:51:38 -0500 Subject: [PATCH 09/21] Coerce WizMote button to string in blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The event delivers the button as a number, so the string comparisons in the choose never matched and no action ran. Cast it with | string. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index c9dc5e4..86ff4fc 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -64,7 +64,9 @@ trigger: event_type: esphome.cast1_wizmote_event variables: - pressed: "{{ trigger.event.data.button }}" + # ESPHome can deliver the button as a number; coerce so the string + # comparisons below ('on', '3', etc.) match. + pressed: "{{ trigger.event.data.button | string }}" action: - choose: From 92f3f0cbdc8dc5e4faa99d6edec5e0ee13da426e Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:03:53 -0500 Subject: [PATCH 10/21] Coerce to string inside choose conditions, not the variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HA's template engine coerces the numeric button "3" to the int 3 when it stores the pressed variable, so a variable-level | string is undone and 3 == '3' stayed false. Move | string into each comparison where the cast survives, so the numeric buttons match. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index 86ff4fc..2e8b195 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -64,27 +64,29 @@ trigger: event_type: esphome.cast1_wizmote_event variables: - # ESPHome can deliver the button as a number; coerce so the string - # comparisons below ('on', '3', etc.) match. - pressed: "{{ trigger.event.data.button | string }}" + # HA's template engine coerces numeric-looking values to numbers, so the + # event's button "3" arrives here as the int 3. The | string in each + # condition below forces the comparison back to text at evaluation time + # (a | string in this variable would just get re-coerced and lost). + pressed: "{{ trigger.event.data.button }}" action: - choose: - - conditions: "{{ pressed == 'on' }}" + - conditions: "{{ pressed | string == 'on' }}" sequence: !input on_action - - conditions: "{{ pressed == 'off' }}" + - conditions: "{{ pressed | string == 'off' }}" sequence: !input off_action - - conditions: "{{ pressed == 'night' }}" + - conditions: "{{ pressed | string == 'night' }}" sequence: !input night_action - - conditions: "{{ pressed == 'brightness_up' }}" + - conditions: "{{ pressed | string == 'brightness_up' }}" sequence: !input bright_up_action - - conditions: "{{ pressed == 'brightness_down' }}" + - conditions: "{{ pressed | string == 'brightness_down' }}" sequence: !input bright_down_action - - conditions: "{{ pressed == '1' }}" + - conditions: "{{ pressed | string == '1' }}" sequence: !input button_1_action - - conditions: "{{ pressed == '2' }}" + - conditions: "{{ pressed | string == '2' }}" sequence: !input button_2_action - - conditions: "{{ pressed == '3' }}" + - conditions: "{{ pressed | string == '3' }}" sequence: !input button_3_action - - conditions: "{{ pressed == '4' }}" + - conditions: "{{ pressed | string == '4' }}" sequence: !input button_4_action From a8f9e1fd9ba325554b37a57accb739a025c5ae15 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:11:04 -0500 Subject: [PATCH 11/21] Route WizMote blueprint with native event triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the template choose with one event trigger per button (event_data filter + trigger id) and condition: trigger. HA matches the raw event value instead of a Jinja-coerced variable, so the numeric buttons can't break, it validates at load instead of failing silently, and quoting the button values avoids the on/off YAML-boolean trap. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 95 +++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index 2e8b195..2c98bf5 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -59,34 +59,91 @@ blueprint: mode: queued max: 10 -trigger: - - platform: event +# One event trigger per button, matched natively on the event's button value +# (no templates, so HA's numeric coercion can't break the match). The button +# values are quoted so "on"/"off" aren't read as YAML booleans. +triggers: + - trigger: event event_type: esphome.cast1_wizmote_event + event_data: + button: "on" + id: "on" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "off" + id: "off" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "night" + id: "night" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "brightness_up" + id: "brightness_up" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "brightness_down" + id: "brightness_down" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "1" + id: "button_1" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "2" + id: "button_2" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "3" + id: "button_3" + - trigger: event + event_type: esphome.cast1_wizmote_event + event_data: + button: "4" + id: "button_4" -variables: - # HA's template engine coerces numeric-looking values to numbers, so the - # event's button "3" arrives here as the int 3. The | string in each - # condition below forces the comparison back to text at evaluation time - # (a | string in this variable would just get re-coerced and lost). - pressed: "{{ trigger.event.data.button }}" - -action: +actions: - choose: - - conditions: "{{ pressed | string == 'on' }}" + - conditions: + - condition: trigger + id: "on" sequence: !input on_action - - conditions: "{{ pressed | string == 'off' }}" + - conditions: + - condition: trigger + id: "off" sequence: !input off_action - - conditions: "{{ pressed | string == 'night' }}" + - conditions: + - condition: trigger + id: "night" sequence: !input night_action - - conditions: "{{ pressed | string == 'brightness_up' }}" + - conditions: + - condition: trigger + id: "brightness_up" sequence: !input bright_up_action - - conditions: "{{ pressed | string == 'brightness_down' }}" + - conditions: + - condition: trigger + id: "brightness_down" sequence: !input bright_down_action - - conditions: "{{ pressed | string == '1' }}" + - conditions: + - condition: trigger + id: "button_1" sequence: !input button_1_action - - conditions: "{{ pressed | string == '2' }}" + - conditions: + - condition: trigger + id: "button_2" sequence: !input button_2_action - - conditions: "{{ pressed | string == '3' }}" + - conditions: + - condition: trigger + id: "button_3" sequence: !input button_3_action - - conditions: "{{ pressed | string == '4' }}" + - conditions: + - condition: trigger + id: "button_4" sequence: !input button_4_action From 704a518e744d284fd52e403451fffa760f1a39f2 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:25:10 -0500 Subject: [PATCH 12/21] Configure WizMote buttons entirely from the blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each button now has its own Action dropdown (same 10 options as the device) plus a custom action, grouped in collapsible sections. On HA start and on save, the blueprint pushes each pick down to the matching WizMote select on the CAST-1 via select.select_option, so the device page no longer needs touching - the blueprint is the source of truth. Local media actions still run on-device; "Send HA Event" runs the button's custom action. Needs a CAST-1 device picker; no firmware change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 297 +++++++++++++++++++++++++-------- 1 file changed, 231 insertions(+), 66 deletions(-) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index 2c98bf5..65d70b6 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -1,116 +1,281 @@ blueprint: name: Apollo CAST-1 WizMote description: > - Run Home Assistant actions from an Apollo CAST-1 WizMote. + Configure all nine Apollo CAST-1 WizMote buttons from one place. - On the CAST-1, set any button's dropdown to "Send HA Event" (Button 3 and 4 - ship that way). Then choose what each button does here. Buttons left on a - built-in action (Play, Volume, etc.) are handled on the device and never - reach this automation. + For each button pick an Action. The media actions (Play, Volume, Next, + etc.) run on the CAST-1 itself and keep working even if Home Assistant is + offline. Pick "Send HA Event" to run your own Home Assistant action + instead, then fill in the action below it. + + This blueprint writes your picks down to the CAST-1 when Home Assistant + starts and whenever you save it, so you never need to open the device + page - the blueprint is the source of truth for the buttons. Choose your + CAST-1 below. domain: automation source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/cast-1-wizmote.yaml input: - on_action: - name: "On" - default: [] - selector: - action: - off_action: - name: "Off" - default: [] - selector: - action: - night_action: - name: "Night" - default: [] - selector: - action: - bright_up_action: - name: "Brightness Up" - default: [] + cast1_device: + name: CAST-1 device + description: The CAST-1 whose WizMote buttons this controls. selector: - action: - bright_down_action: - name: "Brightness Down" - default: [] - selector: - action: - button_1_action: + device: + integration: esphome + button_on: + name: "On button" + icon: mdi:gesture-tap-button + collapsed: true + input: + on_mode: + name: Action + default: "Play" + selector: + select: + mode: dropdown + options: &actions + - "Nothing" + - "Play" + - "Pause" + - "Play / Pause" + - "Next Track" + - "Previous Track" + - "Volume Up" + - "Volume Down" + - "Toggle Light" + - "Send HA Event" + on_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_off: + name: "Off button" + icon: mdi:gesture-tap-button + collapsed: true + input: + off_mode: + name: Action + default: "Pause" + selector: + select: + mode: dropdown + options: *actions + off_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_night: + name: "Night button" + icon: mdi:gesture-tap-button + collapsed: true + input: + night_mode: + name: Action + default: "Toggle Light" + selector: + select: + mode: dropdown + options: *actions + night_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_bright_up: + name: "Brightness Up button" + icon: mdi:gesture-tap-button + collapsed: true + input: + bright_up_mode: + name: Action + default: "Volume Up" + selector: + select: + mode: dropdown + options: *actions + bright_up_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_bright_down: + name: "Brightness Down button" + icon: mdi:gesture-tap-button + collapsed: true + input: + bright_down_mode: + name: Action + default: "Volume Down" + selector: + select: + mode: dropdown + options: *actions + bright_down_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_1: name: "Button 1" - default: [] - selector: - action: - button_2_action: + icon: mdi:gesture-tap-button + collapsed: true + input: + button_1_mode: + name: Action + default: "Previous Track" + selector: + select: + mode: dropdown + options: *actions + button_1_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_2: name: "Button 2" - default: [] - selector: - action: - button_3_action: + icon: mdi:gesture-tap-button + collapsed: true + input: + button_2_mode: + name: Action + default: "Next Track" + selector: + select: + mode: dropdown + options: *actions + button_2_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_3: name: "Button 3" - default: [] - selector: - action: - button_4_action: + icon: mdi:gesture-tap-button + collapsed: true + input: + button_3_mode: + name: Action + default: "Send HA Event" + selector: + select: + mode: dropdown + options: *actions + button_3_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: + button_4: name: "Button 4" - default: [] - selector: - action: + icon: mdi:gesture-tap-button + collapsed: true + input: + button_4_mode: + name: Action + default: "Send HA Event" + selector: + select: + mode: dropdown + options: *actions + button_4_action: + name: Custom action + description: Run when Action is set to "Send HA Event". + default: [] + selector: + action: mode: queued max: 10 -# One event trigger per button, matched natively on the event's button value -# (no templates, so HA's numeric coercion can't break the match). The button -# values are quoted so "on"/"off" aren't read as YAML booleans. +variables: + cast1_device: !input cast1_device + +# Two jobs: +# - on HA start / save (id: sync) push each button's chosen Action down to the +# matching WizMote select on the device, so the device page mirrors this UI. +# - on a button event (the device only emits one when its Action is "Send HA +# Event") run that button's custom action. triggers: + - trigger: homeassistant + event: start + id: sync + - trigger: event + event_type: automation_reloaded + id: sync - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "on" + event_data: { button: "on" } id: "on" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "off" + event_data: { button: "off" } id: "off" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "night" + event_data: { button: "night" } id: "night" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "brightness_up" + event_data: { button: "brightness_up" } id: "brightness_up" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "brightness_down" + event_data: { button: "brightness_down" } id: "brightness_down" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "1" + event_data: { button: "1" } id: "button_1" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "2" + event_data: { button: "2" } id: "button_2" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "3" + event_data: { button: "3" } id: "button_3" - trigger: event event_type: esphome.cast1_wizmote_event - event_data: - button: "4" + event_data: { button: "4" } id: "button_4" actions: - choose: + - conditions: + - condition: trigger + id: sync + sequence: + - repeat: + for_each: + - { suffix: "wizmote_on", option: !input on_mode } + - { suffix: "wizmote_off", option: !input off_mode } + - { suffix: "wizmote_night", option: !input night_mode } + - { suffix: "wizmote_brightness_up", option: !input bright_up_mode } + - { suffix: "wizmote_brightness_down", option: !input bright_down_mode } + - { suffix: "wizmote_button_1", option: !input button_1_mode } + - { suffix: "wizmote_button_2", option: !input button_2_mode } + - { suffix: "wizmote_button_3", option: !input button_3_mode } + - { suffix: "wizmote_button_4", option: !input button_4_mode } + sequence: + - action: select.select_option + continue_on_error: true + target: + entity_id: >- + {{ device_entities(cast1_device) + | select('search', '_' ~ repeat.item.suffix ~ '$') + | list | first }} + data: + option: "{{ repeat.item.option }}" - conditions: - condition: trigger id: "on" From d4e7e2e2413459f6eb04eca569d9f63afea53b80 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:28:31 -0500 Subject: [PATCH 13/21] Filter device picker to CAST-1 models only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the BTN-1 blueprint pattern: filter by manufacturer ApolloAutomation and the CAST-1-W / CAST-1-ETH models so the picker only lists CAST-1s instead of every ESPHome device. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index 65d70b6..8b853f1 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -16,11 +16,18 @@ blueprint: source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/cast-1-wizmote.yaml input: cast1_device: - name: CAST-1 device - description: The CAST-1 whose WizMote buttons this controls. + name: Apollo CAST-1 + description: Select your CAST-1 from the dropdown! selector: device: - integration: esphome + filter: + - integration: esphome + manufacturer: ApolloAutomation + model: CAST-1-W + - integration: esphome + manufacturer: ApolloAutomation + model: CAST-1-ETH + multiple: false button_on: name: "On button" icon: mdi:gesture-tap-button From 38e4dc7ba7ff66b913c18c32c743d3ce9dc9b5b8 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:32:55 -0500 Subject: [PATCH 14/21] Show each button's default action under its Action field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Default: " hint to every button so the shipped behavior is visible even after someone changes the dropdown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/cast-1-wizmote.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/cast-1-wizmote.yaml index 8b853f1..5d16476 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/cast-1-wizmote.yaml @@ -35,6 +35,7 @@ blueprint: input: on_mode: name: Action + description: "Default: Play" default: "Play" selector: select: @@ -63,6 +64,7 @@ blueprint: input: off_mode: name: Action + description: "Default: Pause" default: "Pause" selector: select: @@ -81,6 +83,7 @@ blueprint: input: night_mode: name: Action + description: "Default: Toggle Light" default: "Toggle Light" selector: select: @@ -99,6 +102,7 @@ blueprint: input: bright_up_mode: name: Action + description: "Default: Volume Up" default: "Volume Up" selector: select: @@ -117,6 +121,7 @@ blueprint: input: bright_down_mode: name: Action + description: "Default: Volume Down" default: "Volume Down" selector: select: @@ -135,6 +140,7 @@ blueprint: input: button_1_mode: name: Action + description: "Default: Previous Track" default: "Previous Track" selector: select: @@ -153,6 +159,7 @@ blueprint: input: button_2_mode: name: Action + description: "Default: Next Track" default: "Next Track" selector: select: @@ -171,6 +178,7 @@ blueprint: input: button_3_mode: name: Action + description: "Default: Send HA Event" default: "Send HA Event" selector: select: @@ -189,6 +197,7 @@ blueprint: input: button_4_mode: name: Action + description: "Default: Send HA Event" default: "Send HA Event" selector: select: From ceb6fa2b1861608fda1474e279ac0c4e9044d505 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:38:19 -0500 Subject: [PATCH 15/21] Rename blueprint to CAST-1-WizMote.yaml and tidy description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the file (source_url updated to match) and replace the description blob with scannable emoji-labeled lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ...ast-1-wizmote.yaml => CAST-1-WizMote.yaml} | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) rename Blueprints/{cast-1-wizmote.yaml => CAST-1-WizMote.yaml} (93%) diff --git a/Blueprints/cast-1-wizmote.yaml b/Blueprints/CAST-1-WizMote.yaml similarity index 93% rename from Blueprints/cast-1-wizmote.yaml rename to Blueprints/CAST-1-WizMote.yaml index 5d16476..2086a97 100644 --- a/Blueprints/cast-1-wizmote.yaml +++ b/Blueprints/CAST-1-WizMote.yaml @@ -1,19 +1,17 @@ blueprint: name: Apollo CAST-1 WizMote - description: > - Configure all nine Apollo CAST-1 WizMote buttons from one place. + description: | + 🎛️ **WizMote buttons, all in one place.** - For each button pick an Action. The media actions (Play, Volume, Next, - etc.) run on the CAST-1 itself and keep working even if Home Assistant is - offline. Pick "Send HA Event" to run your own Home Assistant action - instead, then fill in the action below it. + ▶️ On-device - Play, Volume, Next... work even if HA is offline. - This blueprint writes your picks down to the CAST-1 when Home Assistant - starts and whenever you save it, so you never need to open the device - page - the blueprint is the source of truth for the buttons. Choose your - CAST-1 below. + 🏠 Send HA Event - runs that button's Custom action. + + 💾 Auto-applied to your CAST-1 on start and save. + + 👇 Pick your CAST-1 to begin. domain: automation - source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/cast-1-wizmote.yaml + source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/CAST-1-WizMote.yaml input: cast1_device: name: Apollo CAST-1 From 08c89c64dd46f8b88f33c5a7e3b1bb9f77ea9373 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:48:51 -0500 Subject: [PATCH 16/21] Rewrite blueprint description with features and Send HA Event examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lead line, a Features list, and a plain-language explainer of "Send HA Event" with examples (Music Assistant playlist, toggle a room's lights, run a scene). Emojis only on the bullets, not the section headers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/CAST-1-WizMote.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Blueprints/CAST-1-WizMote.yaml b/Blueprints/CAST-1-WizMote.yaml index 2086a97..43a730a 100644 --- a/Blueprints/CAST-1-WizMote.yaml +++ b/Blueprints/CAST-1-WizMote.yaml @@ -1,15 +1,19 @@ blueprint: name: Apollo CAST-1 WizMote description: | - 🎛️ **WizMote buttons, all in one place.** + Make your WizMote control the Apollo CAST-1 exactly how you want. - ▶️ On-device - Play, Volume, Next... work even if HA is offline. + **Features** - 🏠 Send HA Event - runs that button's Custom action. + - 🎵 Every button ships with a sensible CAST-1 media action (Play, Pause, Volume, Next, Previous) + - 🔁 Override any of them right here, no device page needed; defaults shown under each + - 🏠 Or set a button to "Send HA Event" to trigger Home Assistant instead - 💾 Auto-applied to your CAST-1 on start and save. + What's "Send HA Event"? It passes the press to Home Assistant so it can do anything HA can, for example: - 👇 Pick your CAST-1 to begin. + - ▶️ Play a Music Assistant playlist + - 💡 Toggle a room's lights + - 🎬 Run a scene or script domain: automation source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/CAST-1-WizMote.yaml input: From fec6608b493ffd009d001a01962bb6fc815852d1 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:51:49 -0500 Subject: [PATCH 17/21] Reword blueprint feature bullets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clearer phrasing: defaults you can override to suit your needs, override from the dropdown, and "Send HA Event" to set up a custom action. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/CAST-1-WizMote.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Blueprints/CAST-1-WizMote.yaml b/Blueprints/CAST-1-WizMote.yaml index 43a730a..02f91f4 100644 --- a/Blueprints/CAST-1-WizMote.yaml +++ b/Blueprints/CAST-1-WizMote.yaml @@ -5,9 +5,9 @@ blueprint: **Features** - - 🎵 Every button ships with a sensible CAST-1 media action (Play, Pause, Volume, Next, Previous) - - 🔁 Override any of them right here, no device page needed; defaults shown under each - - 🏠 Or set a button to "Send HA Event" to trigger Home Assistant instead + - 🎵 Every button ships with a default (Play, Pause, Volume, Next, Previous) but you're able to override any of them to suit your needs + - 🔁 Override any button you want - select another option from the dropdown. + - 🏠 Or set a button to "Send HA Event" to set up a custom action instead! What's "Send HA Event"? It passes the press to Home Assistant so it can do anything HA can, for example: From 0959698499af35779928e43db4ceea15f5642eff Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:52:44 -0500 Subject: [PATCH 18/21] Combine the two overlapping feature bullets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge the "ships with a default" and "override from the dropdown" bullets into one, since they said the same thing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/CAST-1-WizMote.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Blueprints/CAST-1-WizMote.yaml b/Blueprints/CAST-1-WizMote.yaml index 02f91f4..2dff551 100644 --- a/Blueprints/CAST-1-WizMote.yaml +++ b/Blueprints/CAST-1-WizMote.yaml @@ -5,8 +5,7 @@ blueprint: **Features** - - 🎵 Every button ships with a default (Play, Pause, Volume, Next, Previous) but you're able to override any of them to suit your needs - - 🔁 Override any button you want - select another option from the dropdown. + - 🎵 Every button ships with a default (Play, Pause, Volume, Next, Previous) - override any of them by picking another option from its dropdown - 🏠 Or set a button to "Send HA Event" to set up a custom action instead! What's "Send HA Event"? It passes the press to Home Assistant so it can do anything HA can, for example: From e9301c3d0694415634bccb56539dcafb133ff48e Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:02:48 -0500 Subject: [PATCH 19/21] Move WizMote blueprint to the Blueprints repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lives in ApolloAutomation/Blueprints (CAST-1/CAST-1-WizMote.yaml, PR #16); this PR is now firmware-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Blueprints/CAST-1-WizMote.yaml | 331 --------------------------------- 1 file changed, 331 deletions(-) delete mode 100644 Blueprints/CAST-1-WizMote.yaml diff --git a/Blueprints/CAST-1-WizMote.yaml b/Blueprints/CAST-1-WizMote.yaml deleted file mode 100644 index 2dff551..0000000 --- a/Blueprints/CAST-1-WizMote.yaml +++ /dev/null @@ -1,331 +0,0 @@ -blueprint: - name: Apollo CAST-1 WizMote - description: | - Make your WizMote control the Apollo CAST-1 exactly how you want. - - **Features** - - - 🎵 Every button ships with a default (Play, Pause, Volume, Next, Previous) - override any of them by picking another option from its dropdown - - 🏠 Or set a button to "Send HA Event" to set up a custom action instead! - - What's "Send HA Event"? It passes the press to Home Assistant so it can do anything HA can, for example: - - - ▶️ Play a Music Assistant playlist - - 💡 Toggle a room's lights - - 🎬 Run a scene or script - domain: automation - source_url: https://github.com/ApolloAutomation/CAST-1/blob/main/Blueprints/CAST-1-WizMote.yaml - input: - cast1_device: - name: Apollo CAST-1 - description: Select your CAST-1 from the dropdown! - selector: - device: - filter: - - integration: esphome - manufacturer: ApolloAutomation - model: CAST-1-W - - integration: esphome - manufacturer: ApolloAutomation - model: CAST-1-ETH - multiple: false - button_on: - name: "On button" - icon: mdi:gesture-tap-button - collapsed: true - input: - on_mode: - name: Action - description: "Default: Play" - default: "Play" - selector: - select: - mode: dropdown - options: &actions - - "Nothing" - - "Play" - - "Pause" - - "Play / Pause" - - "Next Track" - - "Previous Track" - - "Volume Up" - - "Volume Down" - - "Toggle Light" - - "Send HA Event" - on_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_off: - name: "Off button" - icon: mdi:gesture-tap-button - collapsed: true - input: - off_mode: - name: Action - description: "Default: Pause" - default: "Pause" - selector: - select: - mode: dropdown - options: *actions - off_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_night: - name: "Night button" - icon: mdi:gesture-tap-button - collapsed: true - input: - night_mode: - name: Action - description: "Default: Toggle Light" - default: "Toggle Light" - selector: - select: - mode: dropdown - options: *actions - night_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_bright_up: - name: "Brightness Up button" - icon: mdi:gesture-tap-button - collapsed: true - input: - bright_up_mode: - name: Action - description: "Default: Volume Up" - default: "Volume Up" - selector: - select: - mode: dropdown - options: *actions - bright_up_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_bright_down: - name: "Brightness Down button" - icon: mdi:gesture-tap-button - collapsed: true - input: - bright_down_mode: - name: Action - description: "Default: Volume Down" - default: "Volume Down" - selector: - select: - mode: dropdown - options: *actions - bright_down_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_1: - name: "Button 1" - icon: mdi:gesture-tap-button - collapsed: true - input: - button_1_mode: - name: Action - description: "Default: Previous Track" - default: "Previous Track" - selector: - select: - mode: dropdown - options: *actions - button_1_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_2: - name: "Button 2" - icon: mdi:gesture-tap-button - collapsed: true - input: - button_2_mode: - name: Action - description: "Default: Next Track" - default: "Next Track" - selector: - select: - mode: dropdown - options: *actions - button_2_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_3: - name: "Button 3" - icon: mdi:gesture-tap-button - collapsed: true - input: - button_3_mode: - name: Action - description: "Default: Send HA Event" - default: "Send HA Event" - selector: - select: - mode: dropdown - options: *actions - button_3_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - button_4: - name: "Button 4" - icon: mdi:gesture-tap-button - collapsed: true - input: - button_4_mode: - name: Action - description: "Default: Send HA Event" - default: "Send HA Event" - selector: - select: - mode: dropdown - options: *actions - button_4_action: - name: Custom action - description: Run when Action is set to "Send HA Event". - default: [] - selector: - action: - -mode: queued -max: 10 - -variables: - cast1_device: !input cast1_device - -# Two jobs: -# - on HA start / save (id: sync) push each button's chosen Action down to the -# matching WizMote select on the device, so the device page mirrors this UI. -# - on a button event (the device only emits one when its Action is "Send HA -# Event") run that button's custom action. -triggers: - - trigger: homeassistant - event: start - id: sync - - trigger: event - event_type: automation_reloaded - id: sync - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "on" } - id: "on" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "off" } - id: "off" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "night" } - id: "night" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "brightness_up" } - id: "brightness_up" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "brightness_down" } - id: "brightness_down" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "1" } - id: "button_1" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "2" } - id: "button_2" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "3" } - id: "button_3" - - trigger: event - event_type: esphome.cast1_wizmote_event - event_data: { button: "4" } - id: "button_4" - -actions: - - choose: - - conditions: - - condition: trigger - id: sync - sequence: - - repeat: - for_each: - - { suffix: "wizmote_on", option: !input on_mode } - - { suffix: "wizmote_off", option: !input off_mode } - - { suffix: "wizmote_night", option: !input night_mode } - - { suffix: "wizmote_brightness_up", option: !input bright_up_mode } - - { suffix: "wizmote_brightness_down", option: !input bright_down_mode } - - { suffix: "wizmote_button_1", option: !input button_1_mode } - - { suffix: "wizmote_button_2", option: !input button_2_mode } - - { suffix: "wizmote_button_3", option: !input button_3_mode } - - { suffix: "wizmote_button_4", option: !input button_4_mode } - sequence: - - action: select.select_option - continue_on_error: true - target: - entity_id: >- - {{ device_entities(cast1_device) - | select('search', '_' ~ repeat.item.suffix ~ '$') - | list | first }} - data: - option: "{{ repeat.item.option }}" - - conditions: - - condition: trigger - id: "on" - sequence: !input on_action - - conditions: - - condition: trigger - id: "off" - sequence: !input off_action - - conditions: - - condition: trigger - id: "night" - sequence: !input night_action - - conditions: - - condition: trigger - id: "brightness_up" - sequence: !input bright_up_action - - conditions: - - condition: trigger - id: "brightness_down" - sequence: !input bright_down_action - - conditions: - - condition: trigger - id: "button_1" - sequence: !input button_1_action - - conditions: - - condition: trigger - id: "button_2" - sequence: !input button_2_action - - conditions: - - condition: trigger - id: "button_3" - sequence: !input button_3_action - - conditions: - - condition: trigger - id: "button_4" - sequence: !input button_4_action From f053160f4d6a89dcea19c2de047f5dc77f08f984 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:03:56 -0500 Subject: [PATCH 20/21] README: document WizMote Home Assistant control + blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6d9ed5d..9077424 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,16 @@ Easily make your speakers smart and cast to multiple devices using Music Assistant and Sendspin! +## WizMote + +The CAST-1 gives you full Home Assistant control over a paired Wiz WizMote. Each of the nine buttons (On, Off, Night, Brightness Up/Down, and 1–4) is a configurable select on the device — assign it Play, Pause, Play / Pause, Next/Previous Track, Volume Up/Down, Toggle Light, or Send HA Event. The defaults give you media controls out of the box. + +Set any button to **Send HA Event** to trigger a custom Home Assistant action — play a Music Assistant playlist, toggle a room's lights, run a scene or script. + +Configure every button without leaving Home Assistant using the [CAST-1 WizMote blueprint](https://github.com/ApolloAutomation/Blueprints/tree/main/CAST-1): + +[![Import Blueprint](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FApolloAutomation%2FBlueprints%2Fblob%2Fmain%2FCAST-1%2FCAST-1-WizMote.yaml) + Links: \ Discord (Support/feedback/discussion/future products): [http://dsc.gg/ApolloAutomation](http://dsc.gg/ApolloAutomation) \ Shop: [https://apolloautomation.com](https://apolloautomation.com) \ From 6ed2c4ecd704696c365871b23a2fff3391f420af Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:17:05 -0500 Subject: [PATCH 21/21] Remove unused IR remote_receiver (IR removed from PCB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trevor removed the IR hardware from the CAST-1 PCB; the remote_receiver on GPIO07 was unused (nothing consumed it) and left dump: all on. Removes it and bumps version to 26.6.22.1. Closes #16 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Integrations/ESPHome/Core.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Integrations/ESPHome/Core.yaml b/Integrations/ESPHome/Core.yaml index 50a0f45..02cd62d 100644 --- a/Integrations/ESPHome/Core.yaml +++ b/Integrations/ESPHome/Core.yaml @@ -1,5 +1,5 @@ substitutions: - version: "26.6.19.3" + version: "26.6.22.1" packages: wizmote: !include wizmote.yaml @@ -207,14 +207,6 @@ select: then: - script.execute: apply_ota_source -remote_receiver: - pin: - number: GPIO07 - inverted: true - mode: - input: true - dump: all - audio_dac: - platform: pcm5122 id: external_dac