Skip to content

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 issued zha/groups/unbind twice (both success: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.

  1. 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.
  2. 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.)
  3. Still nothing / only stop -> go to step 2. Capture zha_event during the hold to confirm Move-vs-Stop with bindings definitely present (re-use scratchpad/zha_capture.py).
  4. 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.
  5. 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 a Move it immediately follows with Stop) instead of a continuous Move ramp like the good switch's move_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_event showing only stop (no move/move_to_level) while the binding is provably present and 3.07 is installed -> defective, RMA.

[Claude Code, Opus 4.8 diagnostic worker]