Diagnosis: Inovelli Studio Spotlight Switch hold-dim failure¶
Opus 4.8 diagnostic worker, 2026-06-29. Read-only ZHA + reversible bind tests against HA 192.168.86.180:8123. Adversarial: I tried to break my own conclusions and discarded the first one.
TL;DR¶
The leading prior theory, "zha/groups/bind reports success:true but the bind never commits to the device binding table" (briefing hypothesis #1), is FALSE. The bad switch accepts and commits bindings. Every readable attribute on the bad unit is byte-identical to a confirmed-working twin running the same firmware. There is no HA-side / configuration / firmware-version difference left to fix. The remaining explanations are (a) a unit-specific firmware/hardware behavioral fault (-> RMA), or (b) the original "sends Stop on hold" capture was confounded (taken while bindings were removed) and the switch will now dim after I re-established the binding. These can only be separated by a physical paddle-hold retest, which is now set up.
What I changed (current device state — important)¶
- BAD switch (
f0:44:d3:ff:fe:6d:a6:9e) is now RE-BOUND to group 4 (studio_spotlights_zigbee, the 8 Hue spots): OnOff(6)+LevelControl(8) on EP2 and EP3. EP2 is the paddle-transmit endpoint, so this is the native paddle hold-dim binding restored. Ready for a clean physical retest. - GOOD switch (
f0:44:d3:ff:fe:6d:e0:e7): I added one temporary test bind (EP2 OnOff -> group 4) to validate the binding counter, then issuedzha/groups/unbindtwice (bothsuccess:true). The ZDO Unbind_req succeeded so the entry is removed. The firmware's bind counter is latching/unreliable (see below) so it still reads 4; ignore that. Please confirm the recessed paddle does NOT also toggle the spotlights; if it does, re-run the unbind. Low risk, fully reversible.
Evidence gathered (read-only unless noted)¶
| Check | GOOD (recessed, works) | BAD (spotlight) | Verdict |
|---|---|---|---|
| Signature (endpoints/clusters/device types) | EP1 dimmable-light; EP2/EP3 dimmer-switch with OUT 6/8/fc31; EP242 GP | identical | same |
Firmware (sw_build_id) |
3.04 (0x01020304) |
3.04 (0x01020304) |
same, and ZHA reports this as latest available |
| All 86 Inovelli fc31 manufacturer params | baseline | identical except device_bind_number (dynamic) and internal_temp_monitor (1° thermal noise) |
same config |
| LevelControl EP1 attrs (Options, OnOffTransTime, OnLevel, DefaultMoveRate=0) | 0 / 25 / 255 / 0 | identical | same |
| Key behavior params (dimming_algorithm, switch_type=0, output_mode=Dimmer, smart_bulb_mode=on, button_delay, quick_start, low/med/high_for_bound, bind_off_to_on_sync, min/max/default level, dim speeds 1/2/5/6) | baseline | all identical | same |
| Bulb side (group-4 cast on/off + color temp) | n/a | works (per briefing) | group membership OK |
| Bind/unbind ZDO ops on BAD | n/a | success:true, and firmware counter moved 0 -> 7 |
bindings DO commit |
Why the "bind never commits" theory died¶
I read device_bind_number (param 51) = 0 on BAD and first suspected an empty binding table. Adversarial check: BAD's 0 is expected — the briefing says binding was intentionally removed. I then re-bound BAD (4 clusters) and re-read: the counter climbed 0 -> 7. Positive control on GOOD: 3 -> 4 after an added bind. So the firmware does register ZHA group binds; the bad unit is not silently rejecting them. Caveat: the counter lags many seconds and latches (GOOD would not drop back to 3 promptly after unbind), so param 51 is a weak indicator and I do not lean on it. There is no stock ZHA WebSocket path to read the raw on-device binding table (no Mgmt_Bind_req exposed; HA device-diagnostics JSON contains endpoints/neighbors/routes but not the binding table), so the counter + ZDO success is the best available proof, and it points to "commits fine."
The one real discriminator (from briefing, NOT reproduced here)¶
Prior live zha_event capture: on HOLD, BAD emitted stop (cluster 8) while GOOD emitted move_to_level_with_on_off [level, transition]. I could not reproduce this (needs a physical paddle hold; I have no physical access). Confound: it is unknown whether BAD's capture was taken with bindings present or after they were removed. A smart-bulb-mode switch with an empty binding table can legitimately emit only a terminal stop/nothing on hold, which would make that capture apples-to-oranges vs GOOD-with-bindings. This is the single most important thing to re-test cleanly.
Root-cause conclusion¶
This is not a Home Assistant, ZHA, configuration, parameter, output-mode, smart-bulb-mode, binding-commit, or firmware-version problem. A byte-identical twin on the same firmware works. Therefore the fault, if it still reproduces with the now-committed binding, is intrinsic to this physical unit (firmware-state corruption that survived factory-reset + OTA, or a hardware paddle/MCU fault in the hold-detection -> LevelControl-Move path) = defective, RMA. The competing benign explanation (prior capture was confounded; it works now) is equally consistent with all my data and must be ruled out first because it costs nothing.
Recommended fix (ladder, cheapest first)¶
- Physical retest NOW (decisive, free). Binding is freshly re-established on BAD -> group 4. Hold the spotlight paddle up, then down. Watch the 8 Hue spots.
- Dims -> solved; the original failure was the removed-binding confound. Done. (Then mirror the exact binding GOOD uses if you want parity; current EP2+EP3 is fine, EP2 is the one that matters.)
- Still nothing / only
stop-> go to step 2. Capturezha_eventduring the hold to confirm Move-vs-Stop with bindings definitely present (re-usescratchpad/zha_capture.py). - Newer firmware. Inovelli shipped 3.05/3.06/3.07 (3.07 = Mar 26 2026); the briefing's OTA only reached 3.04, and ZHA currently advertises 3.04 as "latest" (the ZHA OTA index hasn't picked up 3.07). Group-binding hold-dim has a documented history of firmware bugs (changelog 2.05/2.06/2.08/2.15 all patched "bound device" dimming). Manually provide the 3.07 OTA file to ZHA, flash this unit, retest. Low cost, plausibly fixes a unit-firmware interaction.
- RMA. If a clean hold test with confirmed-present binding still shows no dim after 3.07, the unit is defective. Inovelli honors VZM31-SN warranty; you have an identical working twin as proof the config is correct.
UPDATE 2026-06-29 (physical test by Justin — new symptom, refines diagnosis)¶
Justin held the paddle on the now-rebound bad switch. New, more specific symptom: with the light ON, holding DOWN makes the spots jump quickly to a low level (~10%) and stop there — no smooth ramp. The good recessed switch ramps smoothly. So:
- The bind is working end-to-end — the switch sends a Level command and the bulbs obey (they change level). This kills any remaining "nothing is bound / it only sends Stop" reading.
- The bad switch is sending a jump (a
MoveToLevel-style command with little/no transition, or aMoveit immediately follows withStop) instead of a continuousMoveramp like the good switch'smove_to_level_with_on_off [level, transition].
This narrows it to a command-shape / transition-time difference at runtime, even though every stored dimming-speed and ramp param reads identical to the good twin (P1=25, P2/P5/P6=127, dimming_algorithm identical, min=1/max=255). That mismatch between identical-config and different-runtime-behavior is the smoking gun for a unit-level firmware/hardware fault, but it must be confirmed with a live command capture (below) before RMA.
Decisive test — STAGED, run when home¶
A live zha_event capture script is ready at scratchpad/zha_hold_capture.py (90s window, labels BAD vs GOOD, logs command + args/params + endpoint/cluster to scratchpad/hold_events.jsonl). Procedure: start it, then on the physical switches — BAD hold-down 4s, BAD hold-up 4s, GOOD hold-down 4s, GOOD hold-up 4s. Then read the jsonl and compare the exact LevelControl commands + transition args. Expected discriminator: GOOD emits move/move_to_level_with_on_off with a non-zero transition; BAD emits a jump (transition 0) or move immediately chased by stop. (Justin left home mid-setup on 2026-06-29; capture not yet run.)
Confidence + falsification¶
- Confidence the problem is NOT HA/config/firmware-version-side: high (~0.9). Backed by a full 86-param diff + signature + LevelControl-attr equality against a working twin, and proof that binds commit.
- Confidence on defective-vs-confound: deliberately withheld (~50/50). I refuse to call RMA without the physical retest, because I could not reproduce the failure and every measurement is identical to a working unit.
- What falsifies "defective": the physical hold test dims the spots (-> it was the removed-binding confound), or flashing 3.07 restores dimming.
- What falsifies "fixable": a hold test with
zha_eventshowing onlystop(nomove/move_to_level) while the binding is provably present and 3.07 is installed -> defective, RMA.
[Claude Code, Opus 4.8 diagnostic worker]