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_<proto> 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) <noreply@anthropic.com>
SchiZzA 2026-06-21 21:47:05 +02:00
parent 514ab823d6
commit 72c383ffba
No known key found for this signature in database
5 changed files with 79 additions and 19 deletions

View File

@ -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),

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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
# ---------------------------------------------------------------------------