diff --git a/custom_components/nvidia_shield/connection.py b/custom_components/nvidia_shield/connection.py index 3ac1162..5b4769b 100644 --- a/custom_components/nvidia_shield/connection.py +++ b/custom_components/nvidia_shield/connection.py @@ -276,7 +276,7 @@ class ShieldConnectionManager: try: if not self.connected and not await self._poller.connect(): - self._note_failure(backoff) + await self._note_failure(backoff) return self._jitter(backoff) if self._failures: self._failures = 0 @@ -285,11 +285,11 @@ class ShieldConnectionManager: await self._refresh() return ADB_POLL_INTERVAL except Exception as err: - self._note_failure(backoff) + await self._note_failure(backoff) _LOGGER.debug("Watchdog cycle failed: %s", err) return self._jitter(backoff) - def _note_failure(self, backoff: float) -> None: + async def _note_failure(self, backoff: float) -> None: """Bookkeep a failed cycle: log sparingly, drop stale store, notify.""" self._failures += 1 if self._failures == 1 or self._failures % 6 == 0: @@ -300,6 +300,13 @@ class ShieldConnectionManager: ) if not self._down_since: self._down_since = True + # Reconnect decision (health_check's documented role): close the + # socket so the next cycle rebuilds it from scratch instead of + # trusting shell()'s auto-reconnect on a broken transport. + try: + await self._poller.disconnect() + except Exception: + _LOGGER.debug("disconnect during reconnect decision failed", exc_info=True) self.store.clear(*ALL_FIELDS) self._notify() diff --git a/tests/test_connection.py b/tests/test_connection.py index 1a04042..d59034f 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -219,8 +219,8 @@ class TestReconnectBackoff: # 5→10→20→40→80→160→300 growth with easing; the last must not exceed # the cap (with jitter). assert max(delays) <= 300.0 * 1.25 - assert delays[1] > delays[0] * 1.5 - assert delays[-1] < 300.0 * 1.25 + assert delays[1] >= delays[0] * 1.5 + assert delays[-1] <= 300.0 * 1.25 async def test_repeated_failures_do_not_spam_listeners(self): poller = FakePoller() @@ -245,6 +245,21 @@ class TestReconnectBackoff: assert manager._failures == 0 assert await manager._watchdog_cycle(5.0) == ADB_POLL_INTERVAL + async def test_failed_wake_poll_forces_reconnect(self): + """health_check drives the reconnect decision: a cycle where is_awake + fails closes the socket so the next cycle reconnects from scratch.""" + poller = FakePoller() + poller.connected = True + poller.awake = None # wake poll fails while the flag says "connected" + manager = _manager(poller) + + delay = await manager._watchdog_cycle(5.0) + + assert 5.0 * 0.75 <= delay <= 5.0 * 1.25 # F5-style symmetric bound + assert poller.disconnect_calls == 1 + assert poller.connected is False + assert manager._failures == 1 + async def test_connect_failure_clears_stale_store_state(self): poller = FakePoller() poller.connect_failures = 1