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>
ecowitt_support
SchiZzA 2026-06-21 21:47:05 +02:00
parent b2fbc33821
commit 2e3c2e0ba8
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 ( from .const import (
DEFAULT_URL, DEFAULT_URL,
DOMAIN, DOMAIN,
ECOWITT_ENABLED,
ECOWITT_URL_PREFIX, ECOWITT_URL_PREFIX,
HEALTH_URL, HEALTH_URL,
LEGACY_ENABLED,
POCASI_CZ_ENABLED, POCASI_CZ_ENABLED,
WINDY_ENABLED, WINDY_ENABLED,
WSLINK, WSLINK,
@ -53,9 +55,23 @@ from .windy_func import WindyPush
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _protocol_name(wslink_enabled: bool) -> str: # Protocols that represent a real, accepted ingress (not health / unknown).
"""Return the configured protocol name.""" _REAL_PROTOCOLS: frozenset[str] = frozenset({"wu", "wslink", "ecowitt"})
return "wslink" if wslink_enabled else "wu" _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: 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]: def _default_health_data(config: SWSConfigEntry) -> dict[str, Any]:
"""Build the default health/debug payload for this config entry.""" """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 { return {
"integration_status": f"online_{configured_protocol}", "integration_status": f"online_{configured_protocol}",
"configured_protocol": configured_protocol, "configured_protocol": configured_protocol,
@ -211,18 +227,25 @@ class HealthCoordinator(DataUpdateCoordinator):
accepted = bool(ingress.get("accepted")) accepted = bool(ingress.get("accepted"))
reason = ingress.get("reason") reason = ingress.get("reason")
if (reason in {"route_disabled", "route_not_registered", "unauthorized"}) or ( # A WU vs WSLink mismatch means the station is misconfigured for the legacy
last_protocol in {"wu", "wslink"} and last_protocol != configured_protocol # 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" 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}" integration_status = f"online_{last_protocol}"
else: else:
integration_status = "online_idle" integration_status = "online_idle"
data["integration_status"] = integration_status data["integration_status"] = integration_status
data["active_protocol"] = ( 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]: async def _async_update_data(self) -> dict[str, Any]:
@ -281,7 +304,7 @@ class HealthCoordinator(DataUpdateCoordinator):
def update_routing(self, routes: Routes | None) -> None: def update_routing(self, routes: Routes | None) -> None:
"""Store the currently enabled routes for diagnostics.""" """Store the currently enabled routes for diagnostics."""
data = deepcopy(self.data) 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: if routes is not None:
data["routes"] = { data["routes"] = {
"wu_enabled": routes.path_enabled(DEFAULT_URL), "wu_enabled": routes.path_enabled(DEFAULT_URL),

View File

@ -191,6 +191,7 @@
"state": { "state": {
"online_wu": "Online PWS/WU", "online_wu": "Online PWS/WU",
"online_wslink": "Online WSLink", "online_wslink": "Online WSLink",
"online_ecowitt": "Online Ecowitt",
"online_idle": "Waiting for data", "online_idle": "Waiting for data",
"degraded": "Degraded", "degraded": "Degraded",
"error": "Error" "error": "Error"
@ -200,7 +201,8 @@
"name": "Active protocol", "name": "Active protocol",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"wslink_addon_status": { "wslink_addon_status": {
@ -235,7 +237,8 @@
"name": "Last access protocol", "name": "Last access protocol",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"last_ingress_route_enabled": { "last_ingress_route_enabled": {

View File

@ -192,6 +192,7 @@
"state": { "state": {
"online_wu": "Online PWS/WU", "online_wu": "Online PWS/WU",
"online_wslink": "Online WSLink", "online_wslink": "Online WSLink",
"online_ecowitt": "Online Ecowitt",
"online_idle": "Čekám na data", "online_idle": "Čekám na data",
"degraded": "Degradovaný", "degraded": "Degradovaný",
"error": "Nefunkční" "error": "Nefunkční"
@ -201,7 +202,8 @@
"name": "Aktivní protokol", "name": "Aktivní protokol",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"wslink_addon_status": { "wslink_addon_status": {
@ -236,7 +238,8 @@
"name": "Protokol posledního přístupu", "name": "Protokol posledního přístupu",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"last_ingress_route_enabled": { "last_ingress_route_enabled": {

View File

@ -191,6 +191,7 @@
"state": { "state": {
"online_wu": "Online PWS/WU", "online_wu": "Online PWS/WU",
"online_wslink": "Online WSLink", "online_wslink": "Online WSLink",
"online_ecowitt": "Online Ecowitt",
"online_idle": "Waiting for data", "online_idle": "Waiting for data",
"degraded": "Degraded", "degraded": "Degraded",
"error": "Error" "error": "Error"
@ -200,7 +201,8 @@
"name": "Active protocol", "name": "Active protocol",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"wslink_addon_status": { "wslink_addon_status": {
@ -235,7 +237,8 @@
"name": "Last access protocol", "name": "Last access protocol",
"state": { "state": {
"wu": "PWS/WU", "wu": "PWS/WU",
"wslink": "WSLink API" "wslink": "WSLink API",
"ecowitt": "Ecowitt"
} }
}, },
"last_ingress_route_enabled": { "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 ( from custom_components.sws12500.const import (
DEFAULT_URL, DEFAULT_URL,
DOMAIN, DOMAIN,
ECOWITT_ENABLED,
ECOWITT_URL_PREFIX, ECOWITT_URL_PREFIX,
HEALTH_URL, HEALTH_URL,
LEGACY_ENABLED,
POCASI_CZ_ENABLED, POCASI_CZ_ENABLED,
WINDY_ENABLED, WINDY_ENABLED,
WSLINK, WSLINK,
@ -122,9 +124,14 @@ def _patch_network(monkeypatch, session: _FakeSession, ip: str = "1.2.3.4") -> N
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_protocol_name() -> None: def test_configured_protocol() -> None:
assert hc._protocol_name(True) == "wslink" # Legacy enabled (default) -> wu / wslink based on the WSLINK flag.
assert hc._protocol_name(False) == "wu" 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: 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" 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 # HealthCoordinator construction & persistence
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -262,6 +276,20 @@ def test_refresh_summary_online_idle(hass, entry) -> None:
assert data["active_protocol"] == "wu" 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 # _async_update_data: offline and online paths
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------