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
parent
b2fbc33821
commit
2e3c2e0ba8
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue