fix(coordinator): downgrade observe mode when a device stays unreachable
When a poll fails and the immediate reconnect retry fails too, _async_update_data returned the last-known snapshot as a degraded success (issue #254) without ever touching observe mode. That's fine for the data itself, but the connection-mode sensor reads straight from self._observe.mode, and only the *successful* reconnect branch ever changed it -- so a device that drops off the network entirely (air-gapped, powered off, Wi-Fi down) left that sensor reporting "Push" forever, hours after the session was actually dead (issue #287). Downgrade to poll mode on the failure branch too, without attempting an immediate resubscribe: the reconnect that would normally justify one just proved there's no live session to subscribe on. Recovery still happens on its own once the device is reachable again, via the existing poll-mode retry timer (_maybe_retry_observe_mode).
This commit is contained in:
1 parent
455ed5b27c
commit
252306838d
2 files changed
+65
No files matched your search
@@ -834,6 +834,20 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
resources = await self.hass.async_add_executor_job(self._poll_once)
|
||||
except Exception as e2:
|
||||
self._log.error("poll failed after reconnect: %s", e2)
|
||||
# The reconnect itself proved the old session is gone, so
|
||||
# any OBSERVE subscription on it is too -- this cycle has
|
||||
# no live session to resubscribe on, unlike the success
|
||||
# branch below, so downgrade without setting
|
||||
# just_downgraded_from_observe (that would attempt a
|
||||
# resubscribe this same cycle against a session we just
|
||||
# confirmed is unreachable). Without this, a device that
|
||||
# drops off the network entirely leaves the connection-mode
|
||||
# sensor reporting "Push" forever, since only the success
|
||||
# path below ever changed it (issue #287) -- the poll-mode
|
||||
# retry timer (_maybe_retry_observe_mode) still re-attempts
|
||||
# observe once the device is actually reachable again.
|
||||
if self._observe.mode == MODE_OBSERVE:
|
||||
self._observe.downgrade_to_poll()
|
||||
snapshot = self._cache.snapshot()
|
||||
# Same precondition as _defer_reconnect_for (issue #254):
|
||||
# degraded-but-successful data only makes sense once
|
||||
|
||||
@@ -478,6 +478,57 @@ async def test_reconnect_while_observe_mode_downgrades_to_poll(
|
||||
assert coordinator._observe.mode == MODE_POLL
|
||||
|
||||
|
||||
async def test_total_poll_failure_downgrades_observe_mode_to_poll(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session, fridge_resources
|
||||
) -> None:
|
||||
"""A device that drops off the network entirely -- both the poll and its
|
||||
reconnect retry fail -- must not leave the connection-mode sensor
|
||||
reporting 'Push' forever (issue #287). Only the *successful* reconnect
|
||||
branch used to touch observe mode (see
|
||||
test_reconnect_while_observe_mode_downgrades_to_poll); this covers the
|
||||
branch where the device stays unreachable."""
|
||||
fake = mock_coordinator_observe_session
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
hrefs = coordinator._hot_hrefs + coordinator._warm_hrefs
|
||||
|
||||
fake.notify_on_subscribe = {"notified": True}
|
||||
entered = await hass.async_add_executor_job(
|
||||
coordinator._observe.try_enter_observe_mode,
|
||||
fake,
|
||||
hrefs,
|
||||
0.02,
|
||||
0.8,
|
||||
)
|
||||
assert entered is True
|
||||
assert coordinator.observe_mode == MODE_OBSERVE
|
||||
|
||||
last_notify_ts = coordinator._observe._last_notify_ts
|
||||
assert last_notify_ts is not None
|
||||
coordinator._observe._last_notify_ts = last_notify_ts - (PUSH_HEALTH_WINDOW_S + 1)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"custom_components.localthings.coordinator.LocalThingsCoordinator._poll_once",
|
||||
side_effect=[RuntimeError("connection lost"), RuntimeError("still lost")],
|
||||
),
|
||||
patch(
|
||||
"custom_components.localthings.coordinator.asyncio.sleep",
|
||||
new=AsyncMock(),
|
||||
),
|
||||
):
|
||||
await coordinator.async_request_refresh()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The update still "succeeds" with the last-known snapshot (issue #254's
|
||||
# degraded-data path) -- but the connection mode must reflect reality
|
||||
# now, not the stale OBSERVE state from before the outage.
|
||||
assert coordinator.observe_mode == MODE_POLL
|
||||
assert coordinator.last_update_success is True
|
||||
|
||||
|
||||
async def test_poll_timeout_skips_reconnect_when_push_is_healthy(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user