@@ -2462,7 +2462,7 @@ async def on(eid: str, event: Event) -> None:
24622462 elif state .state == STATE_OFF : # is turning on
24632463 await on (eid , event )
24642464
2465- async def state_changed_event_listener (
2465+ async def state_changed_event_listener ( # noqa: PLR0912
24662466 self ,
24672467 event : Event [EventStateChangedData ],
24682468 ) -> None :
@@ -2539,12 +2539,24 @@ async def state_changed_event_listener(
25392539 if old_on and new_off :
25402540 # Tracks 'on' → 'off' state changes
25412541 self .on_to_off_event [entity_id ] = event
2542- self .reset (entity_id )
2543- _LOGGER .debug (
2544- "Detected an 'on' → 'off' event for '%s' with context.id='%s'" ,
2545- entity_id ,
2546- event .context .id ,
2547- )
2542+ if self .is_proactively_adapting (event .context .id ):
2543+ # A transient 'off' state reported by the device during a
2544+ # proactive turn-on adaptation (e.g., Zigbee devices that
2545+ # briefly report 'off' mid-transition). Do not cancel the
2546+ # in-flight adaptation task — just record the event.
2547+ _LOGGER .debug (
2548+ "Detected an 'on' → 'off' event for '%s' with context.id='%s'"
2549+ " during proactive adaptation, skipping reset" ,
2550+ entity_id ,
2551+ event .context .id ,
2552+ )
2553+ else :
2554+ self .reset (entity_id )
2555+ _LOGGER .debug (
2556+ "Detected an 'on' → 'off' event for '%s' with context.id='%s'" ,
2557+ entity_id ,
2558+ event .context .id ,
2559+ )
25482560 elif old_off and new_on :
25492561 # Tracks 'off' → 'on' state changes
25502562 self .off_to_on_event [entity_id ] = event
@@ -2761,14 +2773,6 @@ async def just_turned_off( # noqa: PLR0911
27612773 )
27622774 return False
27632775
2764- if off_to_on_event .context .id == on_to_off_event .context .id :
2765- _LOGGER .debug (
2766- "just_turned_off: 'on' → 'off' state change has the same context.id as the"
2767- " 'off' → 'on' state change for '%s'. This is probably a false positive." ,
2768- entity_id ,
2769- )
2770- return True
2771-
27722776 id_on_to_off = on_to_off_event .context .id
27732777
27742778 turn_off_event = self .turn_off_event .get (entity_id )
@@ -2777,6 +2781,10 @@ async def just_turned_off( # noqa: PLR0911
27772781 else :
27782782 transition = None
27792783
2784+ # Check if the off→on was triggered by a real turn_on service call
2785+ # BEFORE the context-equality check. This prevents false positives
2786+ # where a Zigbee device briefly reports 'off' during a turn_on
2787+ # transition, causing both events to share the same context ID.
27802788 if self ._off_to_on_state_event_is_from_turn_on (entity_id , off_to_on_event ):
27812789 is_toggle = off_to_on_event == self .toggle_event .get (entity_id )
27822790 from_service = "light.toggle" if is_toggle else "light.turn_on"
@@ -2786,6 +2794,21 @@ async def just_turned_off( # noqa: PLR0911
27862794 )
27872795 return False
27882796
2797+ if (
2798+ off_to_on_event .context .id == on_to_off_event .context .id
2799+ and turn_off_event is not None
2800+ and turn_off_event .context .id == on_to_off_event .context .id
2801+ ):
2802+ # Context IDs match AND a real turn_off service call was made with
2803+ # the same context. This is the legitimate case: a light still
2804+ # transitioning off that briefly polls as 'on'. Safe to cancel.
2805+ _LOGGER .debug (
2806+ "just_turned_off: 'on' → 'off' state change has the same context.id as the"
2807+ " 'off' → 'on' state change for '%s', confirmed by turn_off event." ,
2808+ entity_id ,
2809+ )
2810+ return True
2811+
27892812 if (
27902813 turn_off_event is not None
27912814 and id_on_to_off == turn_off_event .context .id
0 commit comments