Conversation
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)
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)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sync beta up to main
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)
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)
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)
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)
So testers can confirm they flashed this build. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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)
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)
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)
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)
Add WizMote (ESP-NOW) remote control
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)
Adds a "Default: <action>" 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)
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)
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)
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)
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)
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)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
README: document WizMote Home Assistant control + blueprint
Configure the WizMote from Home Assistant (per-button selects + blueprint)
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)
Remove unused IR remote_receiver (IR removed from PCB)
WalkthroughAdds WizMote ESP-NOW remote control support to the Apollo CAST-1. A new ChangesWizMote ESP-NOW Integration
Sequence Diagram(s)sequenceDiagram
participant WizMote as WizMote Remote
participant ESPNOW as espnow (CAST-1)
participant Scripts as process_wizmote_button / run_wizmote_action
participant HA as Home Assistant
rect rgba(100, 149, 237, 0.5)
Note over WizMote,HA: Pairing Flow
WizMote->>ESPNOW: broadcast (unknown peer)
ESPNOW->>ESPNOW: discovery mode ON → capture sender MAC
ESPNOW->>ESPNOW: disable discovery mode, store paired MAC
end
rect rgba(144, 238, 144, 0.5)
Note over WizMote,HA: Button Press Flow
WizMote->>ESPNOW: broadcast packet (paired MAC)
ESPNOW->>ESPNOW: validate size, deduplicate sequence, update last_packet_ms
ESPNOW->>Scripts: process_wizmote_button(button_code)
Scripts->>Scripts: map code → select entity value
Scripts->>Scripts: run_wizmote_action(action string)
Scripts->>HA: media control call / light.toggle / esphome.cast1_wizmote_event
end
rect rgba(255, 165, 0, 0.5)
Note over ESPNOW,HA: Channel Scan (ETH, no recent packet)
ESPNOW->>ESPNOW: every 2 s: increment wizmote_scan_channel (1–13)
ESPNOW->>ESPNOW: espnow.set_channel(wizmote_scan_channel)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Integrations/ESPHome/wizmote.yaml`:
- Around line 463-482: The on_boot block restores the saved WizMote peer by
adding it via espnow.peer.add but fails to record this restored MAC address in
the wizmote_previous_mac variable. Since wizmote_previous_mac has restore_value
set to no, it remains empty after boot, causing the on_value delete branch to
skip removing the old peer if the MAC is later changed, leaving a stale peer.
After the espnow.peer.add action completes in the on_boot block, add an action
to set wizmote_previous_mac to the value of wizmote_mac_address so that future
MAC changes can properly delete the boot-restored peer.
- Around line 204-240: The return statement in the initial MAC validation lambda
only exits that lambda without preventing the subsequent espnow.peer.delete and
espnow.peer.add actions from executing. A malformed MAC value will bypass the
validation guard and still be processed by sscanf and stored. Fix this by
creating a global boolean variable wizmote_mac_valid initialized to false, then
set it to true only after successful MAC validation in the first lambda, and
update the conditions for both the espnow.peer.delete if block and the
espnow.peer.add if block to check this flag alongside their existing conditions
to ensure peer operations only execute when the MAC is valid.
In `@README.md`:
- Around line 8-17: The blueprint import link in the WizMote section contains a
broken URL that returns HTTP 404. Locate the correct URL path for the
CAST-1-WizMote.yaml file in the ApolloAutomation/Blueprints repository and
update the blueprint_url parameter in the redirect link to point to the correct
location. If the blueprint file is no longer available or has been moved, either
remove the entire blueprint import badge and link section, or replace it with
the correct working URL.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 877e8fde-7d2b-4c76-97a5-e5b99c392dda
📒 Files selected for processing (4)
Integrations/ESPHome/CAST-1_ETH.yamlIntegrations/ESPHome/Core.yamlIntegrations/ESPHome/wizmote.yamlREADME.md
| - 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<uint8_t, 6> 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<uint8_t, 6> 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();' |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🌐 Web query:
In ESPHome, does a return; statement inside a lambda action stop the remaining actions in the same automation action list, or only that lambda?
💡 Result:
In ESPHome, a return; statement inside a lambda action only exits the lambda function itself [1][2][3]. It does not stop, cancel, or otherwise influence the remaining actions in the automation's action list [3]. Because the lambda is executed as a single action within the automation's sequence, the control flow of the automation will proceed to the next action in the list immediately after the lambda finishes executing, regardless of whether you used a return; statement inside the lambda [3]. If you need to stop the execution of an entire automation sequence from within a lambda, you cannot do so simply by returning [3]. Instead, you should consider alternatives such as: 1. Using conditional logic (if/else) within your YAML configuration to decide whether subsequent actions should run [1][4]. 2. If the automation is inside a script, you can use the script.stop action [5][6]. 3. Designing your logic such that subsequent actions are wrapped in a conditional action that checks a state variable you might update in your lambda [1][7].
Citations:
- 1: https://new.esphome.io/automations/actions/
- 2: https://esphome.io/automations/actions/
- 3: https://api-docs.esphome.io/classesphome_1_1_lambda_action
- 4: https://esphome.io/automations/templates/
- 5: https://esphome.io/components/script/
- 6: script.stop fails to stop the script esphome/issues#1105
- 7: https://developers.esphome.io/architecture/components/automations/
Invalid-MAC guard does not stop peer operations.
A return; statement inside the validation lambda (lines 204–214) only exits that lambda—it does not stop the remaining actions in the sequence. The subsequent if blocks for espnow.peer.delete and espnow.peer.add (lines 219, 231) will still execute, even when MAC validation fails. A malformed value (e.g., zz:zz:zz:zz:zz:zz) bypasses the guard and reaches sscanf with invalid bytes, and is stored as the previous MAC.
Gate the peer operations on validity by using a global flag:
🐛 Proposed fix
Add a global:
globals:
- id: wizmote_mac_valid
type: bool
restore_value: no
initial_value: 'false'Then set and check it:
- 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; } }
}
+ id(wizmote_mac_valid) = valid;
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");'
+ lambda: 'return id(wizmote_mac_valid) && id(wizmote_previous_mac) != "" && id(wizmote_previous_mac) != "00:00:00:00:00:00";'
then:
- espnow.peer.delete:
@@
- if:
condition:
- lambda: 'return (x != "00:00:00:00:00:00" && x != "");'
+ lambda: 'return id(wizmote_mac_valid) && x != "00:00:00:00:00:00" && x != "";'
then:
- espnow.peer.add:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/wizmote.yaml` around lines 204 - 240, The return
statement in the initial MAC validation lambda only exits that lambda without
preventing the subsequent espnow.peer.delete and espnow.peer.add actions from
executing. A malformed MAC value will bypass the validation guard and still be
processed by sscanf and stored. Fix this by creating a global boolean variable
wizmote_mac_valid initialized to false, then set it to true only after
successful MAC validation in the first lambda, and update the conditions for
both the espnow.peer.delete if block and the espnow.peer.add if block to check
this flag alongside their existing conditions to ensure peer operations only
execute when the MAC is valid.
| 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<uint8_t, 6> 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; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Boot peer restore leaves wizmote_previous_mac unset, leaking the old peer on reconfigure.
wizmote_previous_mac has restore_value: no, so it is "" after boot. The on_boot block adds the saved peer (Line 475) but never records it in wizmote_previous_mac. When the user later changes the MAC, the on_value delete branch (Line 217) sees an empty previous_mac, skips deletion, and the boot-added peer is never removed — a stale ESP-NOW peer accumulates.
🛠️ Record the restored MAC
- espnow.peer.add:
address: !lambda |-
std::string mac = id(wizmote_mac_address).state;
std::array<uint8_t, 6> 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) = id(wizmote_mac_address).state;'📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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<uint8_t, 6> 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; | |
| 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<uint8_t, 6> 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) = id(wizmote_mac_address).state;' |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Integrations/ESPHome/wizmote.yaml` around lines 463 - 482, The on_boot block
restores the saved WizMote peer by adding it via espnow.peer.add but fails to
record this restored MAC address in the wizmote_previous_mac variable. Since
wizmote_previous_mac has restore_value set to no, it remains empty after boot,
causing the on_value delete branch to skip removing the old peer if the MAC is
later changed, leaving a stale peer. After the espnow.peer.add action completes
in the on_boot block, add an action to set wizmote_previous_mac to the value of
wizmote_mac_address so that future MAC changes can properly delete the
boot-restored peer.
| ## 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): | ||
|
|
||
| [](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) | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify the WizMote blueprint file exists and is accessible
BLUEPRINT_URL="https://raw.githubusercontent.com/ApolloAutomation/Blueprints/main/CAST-1/CAST-1-WizMote.yaml"
echo "Checking blueprint accessibility..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BLUEPRINT_URL")
if [[ "$HTTP_CODE" == "200" ]]; then
echo "✓ Blueprint is accessible (HTTP $HTTP_CODE)"
# Verify it's valid YAML
curl -s "$BLUEPRINT_URL" | python -c "import yaml, sys; yaml.safe_load(sys.stdin)" && echo "✓ Valid YAML syntax" || echo "✗ Invalid YAML syntax"
else
echo "✗ Blueprint not accessible (HTTP $HTTP_CODE)"
echo " URL: $BLUEPRINT_URL"
exit 1
fiRepository: ApolloAutomation/CAST-1
Length of output: 336
Update or remove the broken blueprint import link.
The blueprint URL at https://raw.githubusercontent.com/ApolloAutomation/Blueprints/main/CAST-1/CAST-1-WizMote.yaml returns HTTP 404 and is not accessible. Verify the correct location of the CAST-1-WizMote.yaml file in the ApolloAutomation/Blueprints repository and update the link, or remove the import badge if the blueprint is no longer available.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 8 - 17, The blueprint import link in the WizMote
section contains a broken URL that returns HTTP 404. Locate the correct URL path
for the CAST-1-WizMote.yaml file in the ApolloAutomation/Blueprints repository
and update the blueprint_url parameter in the redirect link to point to the
correct location. If the blueprint file is no longer available or has been
moved, either remove the entire blueprint import badge and link section, or
replace it with the correct working URL.
Version: 26.6.22.1
What does this implement/fix?
Sync beta into main for release. Includes:
remote_receiver(IR removed from the PCB)Types of changes
Checklist / Checklijst:
If user-visible functionality or configuration variables are added/modified:
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation