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) <noreply@anthropic.com>
ecowitt_support
SchiZzA 2026-06-21 12:51:14 +02:00
parent 4e597c1fd5
commit 9e8eff16d8
No known key found for this signature in database
2 changed files with 27 additions and 1 deletions

View File

@ -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.") _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.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) 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) routes.set_ingress_observer(coordinator_health.record_dispatch)
coordinator_health.update_routing(routes) coordinator_health.update_routing(routes)
_LOGGER.debug("%s", routes.show_enabled()) _LOGGER.debug("%s", routes.show_enabled())

View File

@ -77,7 +77,12 @@ class Routes:
if key in self.routes: if key in self.routes:
return self.routes[key] 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: if resource is not None:
canonical_key = f"{request.method}:{resource.canonical}" canonical_key = f"{request.method}:{resource.canonical}"
if canonical_key in self.routes: if canonical_key in self.routes:
@ -103,6 +108,24 @@ class Routes:
) )
return 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: def set_ingress_observer(self, observer: IngressObserver | None) -> None:
"""Set a callback notified for every incoming dispatcher request.""" """Set a callback notified for every incoming dispatcher request."""
self._ingress_observer = observer self._ingress_observer = observer