From 2e3c2e0ba84cda3bf81f7813d3c6d79458146e12 Mon Sep 17 00:00:00 2001 From: SchiZzA Date: Sun, 21 Jun 2026 21:47:05 +0200 Subject: [PATCH] fix(health): reflect Ecowitt as a protocol in the diagnostics summary The health model only knew "wu"/"wslink", so with an Ecowitt setup the active_protocol / integration_status fell back to "PWS/WU" even while Ecowitt data was flowing. - Replace _protocol_name() with _configured_protocol() which returns wu / wslink / ecowitt (legacy endpoint takes precedence; ecowitt-only -> ecowitt). - _refresh_summary: treat "ecowitt" as a real accepted protocol (active_protocol and online_ now track it); the WU-vs-WSLink mismatch -> degraded check is scoped to the legacy pair so coexisting Ecowitt is never falsely degraded. - Add the "ecowitt" / "online_ecowitt" entity states to strings/en/cs. 310 passing, 100% coverage; ruff + basedpyright clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sws12500/health_coordinator.py | 43 ++++++++++++++----- custom_components/sws12500/strings.json | 7 ++- .../sws12500/translations/cs.json | 7 ++- .../sws12500/translations/en.json | 7 ++- tests/test_health.py | 34 +++++++++++++-- 5 files changed, 79 insertions(+), 19 deletions(-) diff --git a/custom_components/sws12500/health_coordinator.py b/custom_components/sws12500/health_coordinator.py index 8a7944d..44b22c0 100644 --- a/custom_components/sws12500/health_coordinator.py +++ b/custom_components/sws12500/health_coordinator.py @@ -37,8 +37,10 @@ from homeassistant.util import dt as dt_util from .const import ( DEFAULT_URL, DOMAIN, + ECOWITT_ENABLED, ECOWITT_URL_PREFIX, HEALTH_URL, + LEGACY_ENABLED, POCASI_CZ_ENABLED, WINDY_ENABLED, WSLINK, @@ -53,9 +55,23 @@ from .windy_func import WindyPush _LOGGER = logging.getLogger(__name__) -def _protocol_name(wslink_enabled: bool) -> str: - """Return the configured protocol name.""" - return "wslink" if wslink_enabled else "wu" +# Protocols that represent a real, accepted ingress (not health / unknown). +_REAL_PROTOCOLS: frozenset[str] = frozenset({"wu", "wslink", "ecowitt"}) +_LEGACY_PROTOCOLS: frozenset[str] = frozenset({"wu", "wslink"}) + + +def _configured_protocol(config: SWSConfigEntry) -> str: + """Return the primary configured protocol (wu / wslink / ecowitt). + + The legacy PWS/WSLink endpoint takes precedence when enabled; otherwise an + Ecowitt-only setup reports "ecowitt". (Legacy and Ecowitt can be enabled at the + same time; this just labels the primary protocol for the summary.) + """ + if checked_or(config.options.get(LEGACY_ENABLED), bool, True): + return "wslink" if checked_or(config.options.get(WSLINK), bool, False) else "wu" + if checked_or(config.options.get(ECOWITT_ENABLED), bool, False): + return "ecowitt" + return "wu" def _protocol_from_path(path: str) -> str: @@ -117,7 +133,7 @@ def _empty_forwarding_state(enabled: bool) -> dict[str, Any]: def _default_health_data(config: SWSConfigEntry) -> dict[str, Any]: """Build the default health/debug payload for this config entry.""" - configured_protocol = _protocol_name(checked_or(config.options.get(WSLINK), bool, False)) + configured_protocol = _configured_protocol(config) return { "integration_status": f"online_{configured_protocol}", "configured_protocol": configured_protocol, @@ -211,18 +227,25 @@ class HealthCoordinator(DataUpdateCoordinator): accepted = bool(ingress.get("accepted")) reason = ingress.get("reason") - if (reason in {"route_disabled", "route_not_registered", "unauthorized"}) or ( - last_protocol in {"wu", "wslink"} and last_protocol != configured_protocol - ): + # A WU vs WSLink mismatch means the station is misconfigured for the legacy + # endpoint. Ecowitt coexists with the legacy endpoint, so it never counts as a + # mismatch - it is a valid protocol whenever a payload arrives on its route. + legacy_mismatch = ( + last_protocol in _LEGACY_PROTOCOLS + and configured_protocol in _LEGACY_PROTOCOLS + and last_protocol != configured_protocol + ) + + if (reason in {"route_disabled", "route_not_registered", "unauthorized"}) or legacy_mismatch: integration_status = "degraded" - elif accepted and last_protocol in {"wu", "wslink"}: + elif accepted and last_protocol in _REAL_PROTOCOLS: integration_status = f"online_{last_protocol}" else: integration_status = "online_idle" data["integration_status"] = integration_status data["active_protocol"] = ( - last_protocol if accepted and last_protocol in {"wu", "wslink"} else configured_protocol + last_protocol if accepted and last_protocol in _REAL_PROTOCOLS else configured_protocol ) async def _async_update_data(self) -> dict[str, Any]: @@ -281,7 +304,7 @@ class HealthCoordinator(DataUpdateCoordinator): def update_routing(self, routes: Routes | None) -> None: """Store the currently enabled routes for diagnostics.""" data = deepcopy(self.data) - data["configured_protocol"] = _protocol_name(checked_or(self.config.options.get(WSLINK), bool, False)) + data["configured_protocol"] = _configured_protocol(self.config) if routes is not None: data["routes"] = { "wu_enabled": routes.path_enabled(DEFAULT_URL), diff --git a/custom_components/sws12500/strings.json b/custom_components/sws12500/strings.json index ec56126..4923b6b 100644 --- a/custom_components/sws12500/strings.json +++ b/custom_components/sws12500/strings.json @@ -191,6 +191,7 @@ "state": { "online_wu": "Online PWS/WU", "online_wslink": "Online WSLink", + "online_ecowitt": "Online Ecowitt", "online_idle": "Waiting for data", "degraded": "Degraded", "error": "Error" @@ -200,7 +201,8 @@ "name": "Active protocol", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "wslink_addon_status": { @@ -235,7 +237,8 @@ "name": "Last access protocol", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "last_ingress_route_enabled": { diff --git a/custom_components/sws12500/translations/cs.json b/custom_components/sws12500/translations/cs.json index a09bea3..fc629ec 100644 --- a/custom_components/sws12500/translations/cs.json +++ b/custom_components/sws12500/translations/cs.json @@ -192,6 +192,7 @@ "state": { "online_wu": "Online PWS/WU", "online_wslink": "Online WSLink", + "online_ecowitt": "Online Ecowitt", "online_idle": "Čekám na data", "degraded": "Degradovaný", "error": "Nefunkční" @@ -201,7 +202,8 @@ "name": "Aktivní protokol", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "wslink_addon_status": { @@ -236,7 +238,8 @@ "name": "Protokol posledního přístupu", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "last_ingress_route_enabled": { diff --git a/custom_components/sws12500/translations/en.json b/custom_components/sws12500/translations/en.json index ec56126..4923b6b 100644 --- a/custom_components/sws12500/translations/en.json +++ b/custom_components/sws12500/translations/en.json @@ -191,6 +191,7 @@ "state": { "online_wu": "Online PWS/WU", "online_wslink": "Online WSLink", + "online_ecowitt": "Online Ecowitt", "online_idle": "Waiting for data", "degraded": "Degraded", "error": "Error" @@ -200,7 +201,8 @@ "name": "Active protocol", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "wslink_addon_status": { @@ -235,7 +237,8 @@ "name": "Last access protocol", "state": { "wu": "PWS/WU", - "wslink": "WSLink API" + "wslink": "WSLink API", + "ecowitt": "Ecowitt" } }, "last_ingress_route_enabled": { diff --git a/tests/test_health.py b/tests/test_health.py index 4fbfbf2..5debcc0 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -32,8 +32,10 @@ from custom_components.sws12500 import health_coordinator as hc, health_sensor a from custom_components.sws12500.const import ( DEFAULT_URL, DOMAIN, + ECOWITT_ENABLED, ECOWITT_URL_PREFIX, HEALTH_URL, + LEGACY_ENABLED, POCASI_CZ_ENABLED, WINDY_ENABLED, WSLINK, @@ -122,9 +124,14 @@ def _patch_network(monkeypatch, session: _FakeSession, ip: str = "1.2.3.4") -> N # --------------------------------------------------------------------------- -def test_protocol_name() -> None: - assert hc._protocol_name(True) == "wslink" - assert hc._protocol_name(False) == "wu" +def test_configured_protocol() -> None: + # Legacy enabled (default) -> wu / wslink based on the WSLINK flag. + assert hc._configured_protocol(_make_entry()) == "wu" + assert hc._configured_protocol(_make_entry({WSLINK: True})) == "wslink" + # Ecowitt-only (legacy off, ecowitt on) -> ecowitt. + assert hc._configured_protocol(_make_entry({LEGACY_ENABLED: False, ECOWITT_ENABLED: True})) == "ecowitt" + # Nothing configured -> wu fallback. + assert hc._configured_protocol(_make_entry({LEGACY_ENABLED: False, ECOWITT_ENABLED: False})) == "wu" def test_protocol_from_path_all_branches() -> None: @@ -168,6 +175,13 @@ def test_default_health_data_wu_default() -> None: assert data["integration_status"] == "online_wu" +def test_default_health_data_ecowitt() -> None: + data = hc._default_health_data(_make_entry({LEGACY_ENABLED: False, ECOWITT_ENABLED: True})) + assert data["configured_protocol"] == "ecowitt" + assert data["active_protocol"] == "ecowitt" + assert data["integration_status"] == "online_ecowitt" + + # --------------------------------------------------------------------------- # HealthCoordinator construction & persistence # --------------------------------------------------------------------------- @@ -262,6 +276,20 @@ def test_refresh_summary_online_idle(hass, entry) -> None: assert data["active_protocol"] == "wu" +def test_refresh_summary_online_ecowitt(hass, entry) -> None: + # Ecowitt ingress is a valid protocol: active_protocol must track it (not fall back + # to the legacy "wu"), and it must not be flagged as a WU/WSLink mismatch. + coordinator = HealthCoordinator(hass, entry) + data = hc._default_health_data(entry) + data["configured_protocol"] = "wu" + data["last_ingress"] = {"protocol": "ecowitt", "accepted": True, "reason": "accepted"} + + coordinator._refresh_summary(data) + + assert data["integration_status"] == "online_ecowitt" + assert data["active_protocol"] == "ecowitt" + + # --------------------------------------------------------------------------- # _async_update_data: offline and online paths # ---------------------------------------------------------------------------