Add stall sensor detection.
parent
0142c5412b
commit
2f7d96f8d3
|
|
@ -28,14 +28,16 @@ period where no entities are subscribed, causing stale states until another full
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from py_typecheck import checked, checked_or
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import (
|
||||
DEFAULT_URL,
|
||||
|
|
@ -53,6 +55,7 @@ from .data import SWSConfigEntry, SWSRuntimeData
|
|||
from .health_coordinator import HealthCoordinator
|
||||
from .legacy import update_legacy_battery_issue
|
||||
from .routes import Routes
|
||||
from .staleness import update_stale_sensors_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR]
|
||||
|
|
@ -181,6 +184,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SWSConfigEntry) -> bool:
|
|||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@callback
|
||||
def _check_stale(_now: datetime) -> None:
|
||||
update_stale_sensors_issue(hass, entry)
|
||||
|
||||
entry.async_on_unload(async_track_time_interval(hass, _check_stale, timedelta(hours=1)))
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
update_legacy_battery_issue(hass, entry)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from py_typecheck import checked, checked_or
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import InvalidStateError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .binary_sensor import add_new_binary_sensors
|
||||
from .const import (
|
||||
|
|
@ -45,6 +46,7 @@ from .ecowitt import EcowittBridge
|
|||
from .health_coordinator import HealthCoordinator
|
||||
from .pocasti_cz import PocasiPush
|
||||
from .sensor import add_new_sensors
|
||||
from .staleness import update_stale_sensors_issue
|
||||
from .utils import (
|
||||
anonymize,
|
||||
check_disabled,
|
||||
|
|
@ -139,6 +141,10 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
add_new_binary_sensors(self.hass, self.config, newly_discovered)
|
||||
add_new_sensors(self.hass, self.config, newly_discovered)
|
||||
self.async_set_updated_data(mapped_data)
|
||||
now = dt_util.utcnow()
|
||||
for key in mapped_data:
|
||||
self.config.runtime_data.last_seen[key] = now
|
||||
update_stale_sensors_issue(self.hass, self.config)
|
||||
|
||||
if health:
|
||||
health.update_ingress_result(
|
||||
|
|
@ -299,6 +305,12 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
|||
add_new_binary_sensors(self.hass, self.config, newly_discovered)
|
||||
|
||||
self.async_set_updated_data(remaped_items)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
for key in remaped_items:
|
||||
self.config.runtime_data.last_seen[key] = now
|
||||
update_stale_sensors_issue(self.hass, self.config)
|
||||
|
||||
if health:
|
||||
health.update_ingress_result(
|
||||
webdata,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ hass.data[DOMAIN]["routes"] because it must outlive a single entry reload.
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntityDescription
|
||||
|
|
@ -45,6 +47,10 @@ class SWSRuntimeData:
|
|||
# Health data cache for diagnostics - refreshed by `HealthCoordinator` on each tick.
|
||||
health_data: dict[str, Any] | None = None
|
||||
|
||||
# Staleness tracking - in-memory, resets on reload.
|
||||
started_at: datetime = field(default_factory=dt_util.utcnow)
|
||||
last_seen: dict[str, datetime] = field(default_factory=dict)
|
||||
|
||||
|
||||
# Type alias for typed ConfigEntry
|
||||
type SWSConfigEntry = ConfigEntry[SWSRuntimeData]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
"""Stale sensor detection.
|
||||
|
||||
If a sensor key is in SENSORS_TO_LOAD but its station never reported it (or stopped reporting it
|
||||
for an extended period), the corresponding HA entity becomes Unavailable indefinitely.
|
||||
|
||||
This module raises a Repairs issue listing such stale keys so users can decide whether to remove them.
|
||||
|
||||
Warmup avoids false-positives at integration startup before any payload arrives.
|
||||
The check is in-memory only — restart/reload resets last_seen, but the 1h warmup covers re-population
|
||||
from incoming webhooks.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, SENSORS_TO_LOAD
|
||||
from .data import SWSConfigEntry
|
||||
|
||||
STALE_THRESHOLD: Final = timedelta(hours=24)
|
||||
WARMUP_PERIOD: Final = timedelta(hours=1)
|
||||
|
||||
|
||||
def _stale_sensor_issue_id(entry: SWSConfigEntry) -> str:
|
||||
"""Return Repairs issue id for this config entry."""
|
||||
return f"stale_sensors_{entry.entry_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def _find_stale_keys(entry: SWSConfigEntry) -> list[str]:
|
||||
"""Return keys in SENSORS_TO_LOAD that haven't been seen recently."""
|
||||
runtime = entry.runtime_data
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if (now - runtime.started_at) < WARMUP_PERIOD:
|
||||
return []
|
||||
|
||||
loaded = entry.options.get(SENSORS_TO_LOAD, [])
|
||||
last_seen = runtime.last_seen
|
||||
|
||||
stale: list[str] = []
|
||||
for key in loaded:
|
||||
seen_at = last_seen.get(key)
|
||||
if seen_at is None or (now - seen_at) > STALE_THRESHOLD:
|
||||
stale.append(key)
|
||||
return stale
|
||||
|
||||
|
||||
@callback
|
||||
def update_stale_sensors_issue(hass: HomeAssistant, entry: SWSConfigEntry) -> None:
|
||||
"""Create or clear a Repairs issue for stale sensor keys."""
|
||||
|
||||
issue_id = _stale_sensor_issue_id(entry)
|
||||
stale = _find_stale_keys(entry)
|
||||
|
||||
if stale:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id=issue_id,
|
||||
is_persistent=True,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="stale_sensors_detected",
|
||||
translation_placeholders={
|
||||
"sensors": ", ".join(sorted(stale)),
|
||||
},
|
||||
)
|
||||
else:
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id=issue_id)
|
||||
|
|
@ -426,6 +426,10 @@
|
|||
"legacy_battery_sensor_deprecated": {
|
||||
"title": "Detekovány zastaralé senzory baterií.",
|
||||
"description": "V registru entit byly nalezeny staré senzory baterií ({entities}). Byly nahrazeny binárními senzory baterií a budou odstraněny ve verzi {remove_version}. Smažte prosím staré entity ručně přes Nastavení -> Zařízení a služby -> SWS-12500."
|
||||
},
|
||||
"stale_sensors_detected": {
|
||||
"title": "Detekovány nečinné sensory",
|
||||
"description": "Tyto senzory jsou nastavené, ale nereportují data: {sensors}. V HA budou viditelné jako Nedostupné. Pokud už nejsou přítomné, můžeš je odstranit přes Nastavení –> Zařízení a Služby –> SWS 12500."
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
|
|
|
|||
|
|
@ -479,6 +479,10 @@
|
|||
"legacy_battery_sensor_deprecated": {
|
||||
"title": "Legacy battery sensor detected",
|
||||
"description": "Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings -> Devices & Services -> SWS 12500."
|
||||
},
|
||||
"stale_sensors_detected": {
|
||||
"title": "Stale sensor detected",
|
||||
"description": "These sensors are configured but haven't reported data recently: {sensors}. They will appear as Unavailable in HA. If they are no longer connected, you can remove them via Settings -> Devices & Services -> SWS 12500."
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue