From 2c8a765036a0530132ac71340ea5f019d8e34646 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Mon, 27 Jul 2026 13:40:32 +0000 Subject: [PATCH] Downgrade a lone poll-failure reconnect to info (issue #119) Samsung's firmware occasionally drops the DTLS session briefly -- normal appliance-side behavior per the README's "Known device behavior" section -- so the coordinator recovering from that on its own doesn't need a WARNING. Only escalate once reconnects pile up within a trailing 60s window (5+), matching the README's own "more than a handful per minute" definition of an actually broken connection. --- custom_components/localthings/coordinator.py | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/custom_components/localthings/coordinator.py b/custom_components/localthings/coordinator.py index dc9e960..b483efe 100644 --- a/custom_components/localthings/coordinator.py +++ b/custom_components/localthings/coordinator.py @@ -85,6 +85,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): _OBSERVE_GRACE_PERIOD_S: float = GRACE_PERIOD_S _RECONNECT_PAUSE_S: float = 5.0 + # A single reconnect is normal appliance-side behavior (see the + # README's "Known device behavior" section) -- Samsung's firmware + # drops the DTLS session briefly every now and then, and the + # coordinator recovering from that on its own isn't something a user + # needs to see at WARNING. Only escalate once reconnects pile up + # within a short window, matching the README's own "more than a + # handful per minute" definition of an actually-broken connection + # (issue #119). + _RECONNECT_WARN_WINDOW_S: float = 60.0 + _RECONNECT_WARN_THRESHOLD: int = 5 + # A block-level ACK timeout on the summary GET doesn't prove the # session is dead (see _poll_once) — require this many in a row # before treating it as one. A single slow transfer on an otherwise @@ -136,6 +147,7 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.one_ui_version: str = '' self._consecutive_poll_timeouts = 0 self._unbound_hrefs: list[str] = [] + self._reconnect_times: list[float] = [] # ------------------------------------------------------------------ # Session management (all blocking — must run in executor) @@ -444,6 +456,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._consecutive_poll_timeouts += 1 return self._consecutive_poll_timeouts < self._POLL_TIMEOUT_LIMIT + def _reconnect_is_frequent(self) -> bool: + """True once this cycle's reconnect is the Nth within the trailing + warn window -- see _RECONNECT_WARN_WINDOW_S/_RECONNECT_WARN_THRESHOLD.""" + now = time.monotonic() + self._reconnect_times = [ + t for t in self._reconnect_times + if now - t < self._RECONNECT_WARN_WINDOW_S + ] + self._reconnect_times.append(now) + return len(self._reconnect_times) >= self._RECONNECT_WARN_THRESHOLD + # ------------------------------------------------------------------ # DataUpdateCoordinator hook # ------------------------------------------------------------------ @@ -470,7 +493,13 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._consecutive_poll_timeouts = 0 # One reconnect attempt — pause briefly so the device can # clean up its DTLS session state before we knock again. - self._log.warning("poll failed, reconnecting: %s", e) + # A lone reconnect is routine (see the README's "Known + # device behavior" section); only warn once they're piling + # up within the trailing window. + if self._reconnect_is_frequent(): + self._log.warning("poll failed, reconnecting: %s", e) + else: + self._log.info("poll failed, reconnecting: %s", e) await self.hass.async_add_executor_job(self._close_session) await asyncio.sleep(self._RECONNECT_PAUSE_S) try: