From 3be19028b104c4aad5de28775cdbd1fd1534bc69 Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 22 Jul 2026 19:19:21 +0300 Subject: [PATCH] Run interface poll on the event loop (fix async_dispatcher_send off-thread) The interface-status poll was registered as a plain lambda, so async_track_time_interval ran it in an executor thread, where its _push_state()->async_dispatcher_send() call tripped HA's thread-safety guard. Make refresh_interface_status a @callback (it only reads fast in-memory interface attributes) and pass it to async_track_time_interval directly. Co-Authored-By: Claude Opus 4.8 --- custom_components/reticulum/__init__.py | 6 ++++-- custom_components/reticulum/manifest.json | 2 +- custom_components/reticulum/reticulum_client.py | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/custom_components/reticulum/__init__.py b/custom_components/reticulum/__init__.py index 302c504..decb0fb 100644 --- a/custom_components/reticulum/__init__.py +++ b/custom_components/reticulum/__init__.py @@ -114,10 +114,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _safe_announce(manager) _schedule_periodic(hass, entry, runtime) - # Poll interface status. + # Poll interface status. refresh_interface_status is a @callback, so pass it + # directly (a lambda would be a plain sync job and get run in an executor + # thread, where its async_dispatcher_send call is not thread-safe). runtime.unsubs.append( async_track_time_interval( - hass, lambda _now: manager.refresh_interface_status(), INTERFACE_POLL + hass, manager.refresh_interface_status, INTERFACE_POLL ) ) diff --git a/custom_components/reticulum/manifest.json b/custom_components/reticulum/manifest.json index cf2b6c7..f9f408f 100644 --- a/custom_components/reticulum/manifest.json +++ b/custom_components/reticulum/manifest.json @@ -12,5 +12,5 @@ "loggers": ["RNS", "LXMF"], "requirements": ["rns>=0.9.0", "lxmf>=0.6.0"], "single_config_entry": true, - "version": "1.0.4" + "version": "1.0.5" } diff --git a/custom_components/reticulum/reticulum_client.py b/custom_components/reticulum/reticulum_client.py index 13f54dc..2e1726a 100644 --- a/custom_components/reticulum/reticulum_client.py +++ b/custom_components/reticulum/reticulum_client.py @@ -612,10 +612,13 @@ class ReticulumManager: # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ - def refresh_interface_status(self) -> None: + @callback + def refresh_interface_status(self, now: Any = None) -> None: """Recompute interface online status and collect telemetry. - Called on the event loop from a periodic timer. RX/TX byte counters and + Decorated with @callback and used directly as the async_track_time_interval + action so it runs on the event loop (only fast in-memory interface + attribute reads happen here, no blocking I/O). RX/TX byte counters and bitrate are available for the TCP client interface; RSSI/SNR/quality are only populated when the underlying interface is a physical one (e.g. an RNode/LoRa interface) and stay ``None`` for a plain TCP link.