From e20c23a3af2c730b6b2518c39ed5792723c556bf Mon Sep 17 00:00:00 2001 From: SchiZzA Date: Sun, 21 Jun 2026 12:51:14 +0200 Subject: [PATCH] fix(routes): rebind sticky health route on reload; defensive resolve After a config reload a new HealthCoordinator is created, but the sticky /station/health route kept calling the previous (stale) instance. Add Routes.rebind_handler() and call it from async_setup_entry's reload branch so the endpoint always serves the current coordinator. Also make _resolve_route() tolerant of requests without match_info/route/ resource instead of raising AttributeError. Co-Authored-By: Claude Opus 4.8 (1M context) --- custom_components/sws12500/__init__.py | 3 +++ custom_components/sws12500/routes.py | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/custom_components/sws12500/__init__.py b/custom_components/sws12500/__init__.py index 4e9ebfb..175849d 100644 --- a/custom_components/sws12500/__init__.py +++ b/custom_components/sws12500/__init__.py @@ -167,6 +167,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: SWSConfigEntry) -> bool: _LOGGER.debug("We have routes registered, will try to switch dispatcher.") routes.switch_route(coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL, enabled=_legacy) routes.set_ecowitt_enabled(_ecowitt_path, coordinator.received_ecowitt_data, _ecowitt_enabled) + # Rebind the sticky health route to the new coordinator so /station/health + # does not keep serving the previous (stale) HealthCoordinator after a reload. + routes.rebind_handler(HEALTH_URL, coordinator_health.health_status) routes.set_ingress_observer(coordinator_health.record_dispatch) coordinator_health.update_routing(routes) _LOGGER.debug("%s", routes.show_enabled()) diff --git a/custom_components/sws12500/routes.py b/custom_components/sws12500/routes.py index 842413b..4925509 100644 --- a/custom_components/sws12500/routes.py +++ b/custom_components/sws12500/routes.py @@ -77,7 +77,12 @@ class Routes: if key in self.routes: return self.routes[key] - resource = request.match_info.route.resource + # Fallback to the aiohttp resource canonical URL (for routes with a path + # parameter such as {webhook_id}). Resolve defensively: a request without + # match_info/route/resource simply has no canonical match. + match_info = getattr(request, "match_info", None) + route = getattr(match_info, "route", None) + resource = getattr(route, "resource", None) if resource is not None: canonical_key = f"{request.method}:{resource.canonical}" if canonical_key in self.routes: @@ -103,6 +108,24 @@ class Routes: ) return + def rebind_handler(self, url_path: str, handler: Handler) -> None: + """Repoint an always-on sticky route to a new handler after a reload. + + Sticky routes (e.g. health) stay enabled across reloads, but their stored + handler is a bound method tied to a specific coordinator instance. When the + integration reloads, a new coordinator is created, so the handler must be + repointed - otherwise the route keeps calling the old (stale) instance. + + Unlike `set_ecowitt_enabled`, this never changes `enabled`; it only rebinds + currently enabled sticky routes for `url_path`. + """ + + for route in self.routes.values(): + if route.url_path == url_path and route.sticky and route.enabled: + route.handler = handler + _LOGGER.debug("Rebound sticky route handler for %s", url_path) + return + def set_ingress_observer(self, observer: IngressObserver | None) -> None: """Set a callback notified for every incoming dispatcher request.""" self._ingress_observer = observer