The text below is generated by Claude (max effort) as the LLM was the one performing the investigation and eventually fixed the issue.
I've read it and it does match what actually happened. I can confirm the "workaround" section is working for me right now.
The actual device that this is happening on is MOES ZigBee Star Feather Wall Light Switch, 1 Gang although Claude thinks its a broader issue, which I can't confirm or deny.
I'm also using this patch to enable actually turning the switch on/off: zigpy/zha-device-handlers#5015
File: zha/application/platforms/light/init.py
Symptom
On a pure on/off Zigbee light (no Level or Color cluster), a light.turn_off followed within ~1.5 s by light.turn_on results in the entity state correctly transitioning off → on, then silently flipping back to off ~1.5 s after the original turn_off, despite the device being physically on and never receiving any further command.
State history of a turn_on, turn_off, turn_on rapid sequence (200 ms gaps):
T+0 state=on (cmd 1)
T+0.22s state=off (cmd 2)
T+0.45s state=on (cmd 3)
T+1.73s state=off ← spurious; same context_id as cmd 3's call_service
The recorder's events/states tables show one call_service event tied to two state changes via the same context_id. No additional service call, automation, or Read_Attributes runs in the gap.
Root cause
async_turn_off unconditionally sets _transitioning_individual = True and schedules async_transition_complete for DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT (~1.5 s):
# zha/application/platforms/light/__init__.py — async_turn_off
if self._zha_config_enable_light_transitioning_flag:
self.async_transition_set_flag()
...
if self._zha_config_enable_light_transitioning_flag:
self.async_transition_start_timer(transition_time) # ~1.5 s
While that flag is set, handle_cluster_handler_attribute_updated buffers every on_off event instead of writing state:
if self.is_transitioning:
if not event.attribute_value:
self._transition_brightness_buffer = 0
return
So cmd 2's optimistic on_off=False event sets _transition_brightness_buffer = 0. Cmd 3's on_off=True event is also buffered (and discarded, since the if not event.attribute_value check is False) — but it does not clear the existing 0. Cmd 3 directly writes self._state = True (line 541 of _async_turn_on_impl's simple-on path), but doesn't touch the buffer.
When cmd 2's timer fires, async_transition_complete reads the still-zero buffer and overrides _state back to False:
if self._transition_brightness_buffer is not None:
if self._transition_brightness_buffer == 0:
# The device is actually off; override the optimistic on-state.
self._state = False
The bug is the asymmetry between async_turn_off (always sets the flag) and async_turn_on (only sets it for brightness/color paths). A turn_off-then-turn_on within the 1.5 s window leaves cmd 2's "off intent" able to override cmd 3's "on" outcome.
Affected scope
Any on/off-only light when light_transitioning_flag is enabled (it's on by default for many users). The bug doesn't depend on a specific quirk — reproduces with stock TZ3000* TS0011/TS0012 switches too. Discovered while debugging a Moes _TZE200_stvgmdjz TS0601, but isolated by disabling the flag globally and re-running the same rapid-toggle test on a TZ3000* device.
Proposed fix
Pick one:
Mirror the asymmetry — in async_turn_off, only set the flag if brightness_supported. Matches the set_transition_flag predicate in async_turn_on. Minimal change; preserves the flag mechanic for the lights that actually need it.
Clear buffer on later opposing event — in the is_transitioning branch of handle_cluster_handler_attribute_updated, when event.attribute_value is truthy, set _transition_brightness_buffer = None. Slightly broader; addresses the root buffering inconsistency.
Skip the buffer override in async_transition_complete for non-dimmable lights — guard the _state = False write with brightness_supported. Safest but most narrow.
I'd lean toward (1).
Workaround
Set light_transitioning_flag: false in ZHA options (Settings → Devices & Services → ZHA → Configure). The flag has no useful function for on/off-only lights and disabling it fully fixes the symptom.
Affected version
Latest version installed via Zigbee Home Automation in HomeAssistant as of creation of this issue (couldn't find an actual version number).
The text below is generated by Claude (max effort) as the LLM was the one performing the investigation and eventually fixed the issue.
I've read it and it does match what actually happened. I can confirm the "workaround" section is working for me right now.
The actual device that this is happening on is MOES ZigBee Star Feather Wall Light Switch, 1 Gang although Claude thinks its a broader issue, which I can't confirm or deny.
I'm also using this patch to enable actually turning the switch on/off: zigpy/zha-device-handlers#5015
File: zha/application/platforms/light/init.py
Symptom
On a pure on/off Zigbee light (no Level or Color cluster), a light.turn_off followed within ~1.5 s by light.turn_on results in the entity state correctly transitioning off → on, then silently flipping back to off ~1.5 s after the original turn_off, despite the device being physically on and never receiving any further command.
State history of a turn_on, turn_off, turn_on rapid sequence (200 ms gaps):
T+0 state=on (cmd 1)
T+0.22s state=off (cmd 2)
T+0.45s state=on (cmd 3)
T+1.73s state=off ← spurious; same context_id as cmd 3's call_service
The recorder's events/states tables show one call_service event tied to two state changes via the same context_id. No additional service call, automation, or Read_Attributes runs in the gap.
Root cause
async_turn_off unconditionally sets _transitioning_individual = True and schedules async_transition_complete for DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT (~1.5 s):
When cmd 2's timer fires, async_transition_complete reads the still-zero buffer and overrides _state back to False:
The bug is the asymmetry between async_turn_off (always sets the flag) and async_turn_on (only sets it for brightness/color paths). A turn_off-then-turn_on within the 1.5 s window leaves cmd 2's "off intent" able to override cmd 3's "on" outcome.
Affected scope
Any on/off-only light when light_transitioning_flag is enabled (it's on by default for many users). The bug doesn't depend on a specific quirk — reproduces with stock TZ3000* TS0011/TS0012 switches too. Discovered while debugging a Moes _TZE200_stvgmdjz TS0601, but isolated by disabling the flag globally and re-running the same rapid-toggle test on a TZ3000* device.
Proposed fix
Pick one:
Mirror the asymmetry — in async_turn_off, only set the flag if brightness_supported. Matches the set_transition_flag predicate in async_turn_on. Minimal change; preserves the flag mechanic for the lights that actually need it.
Clear buffer on later opposing event — in the is_transitioning branch of handle_cluster_handler_attribute_updated, when event.attribute_value is truthy, set _transition_brightness_buffer = None. Slightly broader; addresses the root buffering inconsistency.
Skip the buffer override in async_transition_complete for non-dimmable lights — guard the _state = False write with brightness_supported. Safest but most narrow.
I'd lean toward (1).
Workaround
Set light_transitioning_flag: false in ZHA options (Settings → Devices & Services → ZHA → Configure). The flag has no useful function for on/off-only lights and disabling it fully fixes the symptom.
Affected version
Latest version installed via Zigbee Home Automation in HomeAssistant as of creation of this issue (couldn't find an actual version number).