@@ -490,16 +490,18 @@ async def test_entities_ready_cleared_when_value_becomes_none():
490490# --- state_reported and reconnect behavior --------------------------------
491491
492492
493- async def test_state_reported_event_wakes_wait_for_next_message ():
493+ @pytest .mark .parametrize ("ts_key" , ["lu" , "lc" ])
494+ async def test_state_reported_event_wakes_wait_for_next_message (ts_key : str ):
494495 """HA's ``subscribe_entities`` omits ``s`` from the diff when a sensor
495496 is reported with an unchanged value (only ``lu``/``lc`` updates).
496497 ``wait_for_next_message`` must still wake on those so callers like the
497498 Shelly emulator don't time out on constant sensors that the
498- integration is still actively reporting.
499+ integration is still actively reporting — and the cached numeric value
500+ must remain unchanged (the keepalive carries no new ``s``).
499501 """
500502 pm = _create_powermeter ()
501503 await _simulate_auth_and_states (
502- pm , [{"entity_id" : "sensor.current_power" , "state" : "0 " }]
504+ pm , [{"entity_id" : "sensor.current_power" , "state" : "42 " }]
503505 )
504506 pm ._message_event .clear ()
505507 waiter = asyncio .create_task (pm .wait_for_next_message (timeout = 1 ))
@@ -512,12 +514,14 @@ async def test_state_reported_event_wakes_wait_for_next_message():
512514 {
513515 "type" : "event" ,
514516 "event" : {
515- "c" : {"sensor.current_power" : {"+" : {"lu" : 1000.0 }}},
517+ "c" : {"sensor.current_power" : {"+" : {ts_key : 1000.0 }}},
516518 },
517519 }
518520 ),
519521 )
520522 await waiter # would raise TimeoutError if state_reported didn't wake it
523+ # Keepalive carries no ``s``; the cached value must be preserved.
524+ assert pm ._entity_values ["sensor.current_power" ] == 42.0
521525
522526
523527async def test_state_reported_before_initial_value_is_ignored ():
@@ -547,26 +551,26 @@ async def test_state_reported_before_initial_value_is_ignored():
547551
548552
549553async def test_reconnect_invalidates_cached_values ():
550- """A websocket disconnect must invalidate cached values and clear the
551- ready flag, so ``get_powermeter_watts`` raises and
552- ``wait_for_message`` blocks again until the reconnected
553- ``subscribe_entities`` snapshot arrives. Without this, callers would
554- serve potentially-old values for the duration of the reconnect.
554+ """A websocket disconnect must invalidate cached values, clear the
555+ ready flag, and reset the protocol counter so the reconnected
556+ ``subscribe_entities`` snapshot is what callers see — not stale
557+ cache. Drives the real ``_reset_for_reconnect`` method that
558+ ``_ws_loop`` invokes after a disconnect, so a regression in any of
559+ its four resets is caught here.
555560 """
556561 pm = _create_powermeter ()
557562 await _simulate_auth_and_states (
558563 pm , [{"entity_id" : "sensor.current_power" , "state" : "100" }]
559564 )
565+ pm ._subscribe_entities_id = 42 # non-default; the reset must clear it
560566 assert pm ._entities_ready .is_set ()
561567 assert await pm .get_powermeter_watts () == [100.0 ]
562568
563- # Simulate the reset block in ``_ws_loop`` after a disconnect.
564- pm ._msg_id = 0
565- pm ._subscribe_entities_id = None
566- for eid in list (pm ._entity_values ):
567- pm ._entity_values [eid ] = None
568- pm ._entities_ready .clear ()
569+ pm ._reset_for_reconnect ()
569570
571+ assert pm ._msg_id == 0
572+ assert pm ._subscribe_entities_id is None
573+ assert pm ._entity_values ["sensor.current_power" ] is None
570574 assert not pm ._entities_ready .is_set ()
571575 with pytest .raises (ValueError ):
572576 await pm .get_powermeter_watts ()
@@ -576,3 +580,33 @@ async def test_reconnect_invalidates_cached_values():
576580 )
577581 assert pm ._entities_ready .is_set ()
578582 assert await pm .get_powermeter_watts () == [250.0 ]
583+
584+
585+ async def test_unavailable_blocks_wait_for_message ():
586+ """When a sensor transitions to ``unavailable`` mid-stream, the ready
587+ flag must clear so ``wait_for_message`` blocks again — callers
588+ waiting for a usable reading shouldn't see the immediate return
589+ they'd get from a fully-ready snapshot.
590+ """
591+ pm = _create_powermeter ()
592+ await _simulate_auth_and_states (
593+ pm , [{"entity_id" : "sensor.current_power" , "state" : "100" }]
594+ )
595+ await pm .wait_for_message (timeout = 1 ) # returns immediately when ready
596+
597+ ws = AsyncMock ()
598+ await pm ._handle_message (
599+ ws ,
600+ json .dumps (
601+ {
602+ "type" : "event" ,
603+ "event" : {
604+ "c" : {"sensor.current_power" : {"+" : {"s" : "unavailable" }}},
605+ },
606+ }
607+ ),
608+ )
609+
610+ assert pm ._entity_values ["sensor.current_power" ] is None
611+ with pytest .raises (TimeoutError ):
612+ await pm .wait_for_message (timeout = 0.05 )
0 commit comments