SWS-12500-custom-component/custom_components/sws12500/staleness.py

77 lines
2.4 KiB
Python

"""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)