Skip to content

Sign requests for firmware that requires it - #216

Open
ThomasMcCrossin wants to merge 1 commit into
sbidy:masterfrom
ThomasMcCrossin:signed-requests
Open

ThomasMcCrossin wants to merge 1 commit into
sbidy:masterfrom
ThomasMcCrossin:signed-requests

Conversation

@ThomasMcCrossin

Copy link
Copy Markdown

Fixes #213.

Newer WiZ firmware answers with "env":"pro" and an hmac on every reply. Reads still work unsigned, but any setPilot without a signature comes back as -32602 Invalid params, and a bad signature gets -32605 Security error. My ESP25_SHORGB_01 strip moved from 1.31.0 to 1.39.0 the day I set it up and has behaved this way on both versions.

The key lives in the WiZ app. It can export your home for local integration, and that JSON has a udp_signing_key (32 hex characters, one per home). Requests are signed by adding sigTs (unix seconds) to params and then sending "env":"pro" plus hmac, which is base64 HMAC-SHA256 of the compact params JSON keyed with the hex-decoded key. Replies are signed the same way over the result/error object. I confirmed the scheme on 10 captured replies before sending any writes.

This PR adds:

  • an optional signing_key on wizlight(), which makes send() sign every request
  • utils.sign_request()
  • tests/test_signing.py with a known-answer test and a small fake bulb that rejects unsigned and wrongly signed writes like the real device does
  • a README section

Without a key nothing changes, so older bulbs are unaffected.

On the real strip, turn_on with rgb, turn_off and colortemp all work with the key and fail with -32602 without it. The full suite passes (94 tests), and mypy and black are clean.

Some device behaviour worth knowing for Home Assistant:

  • sigTs 30 seconds old is accepted and 60 seconds old is rejected. Future timestamps and replays were both accepted.
  • With a scene running, {"state": true} on its own can return success without changing the light. A colour or temp in the same call clears the scene.
  • Signed reads work too, and registration still works unsigned, so push updates are fine.

Home Assistant would need a place to enter the key in the WiZ config flow. I can follow up there once this is released.

🤖 Generated with Claude Code

Newer firmware tags replies with "env":"pro" and an hmac. Reads still
work unsigned, but setPilot without a signature fails with -32602 and a
wrong signature fails with -32605.

wizlight() takes an optional signing_key, the udp_signing_key from the
WiZ app's local integration export. When set, send() adds sigTs to
params and signs the compact params JSON with HMAC-SHA256.

Tested against an ESP25_SHORGB_01 strip on 1.39.0.

Fixes sbidy#213

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0x416d616e pushed a commit to 0x416d616e/core that referenced this pull request Oct 11, 2026
Newer WiZ firmware can be set to only accept verified controls, which
makes it reject unsigned setPilot commands (-32602). Commands then need
to be signed with the home security key from the WiZ app.

Store an optional signing key in the config entry and pass it to
pywizlight, which signs every request when it is set. Without a key,
commands stay unsigned, which works for homes that allow all controls.
The key can be entered when setting up a device manually, and changed
or removed with a new reconfigure step. It is redacted from
diagnostics.

The user step is now a menu that also offers importing the local
integration file exported from the WiZ app (format version 1.0). The
import adds the key to the configured devices listed in the file, and
starts discovery flows carrying the key and the WiZ app name for listed
devices that are not set up yet. This replaces pending discovery flows
without the key, including those of devices the network scan misses.
Device entries without a usable MAC address are skipped.

Entries updated by discovery or the manual step are reloaded by their
update listener when loaded, and reloaded right away otherwise.

Requires pywizlight with signing support (sbidy/pywizlight#216), which
is not released yet; the requirement still needs to be bumped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

setPilot always returns "Invalid params" (-32602) on firmware 1.34.1 (ESP25_SHRGB_01) — reads work, writes never do

1 participant