Add sensor freshness checks

This commit is contained in:
buzera committed 2026-10-26 18:13:00 +02:00
1 parent c434f3995d
commit cea79b6b48
12 files changed
+213 -25

No files matched your search

+40 -13
View File
@@ -13,10 +13,12 @@ from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
CONF_ROOM_MAX_AGE, CONF_WEATHER_MAX_AGE,
DOMAIN, CONF_CLIMATE, CONF_HEATER, CONF_ROOMS, CONF_WEIGHTS, WEATHER_SOURCES,
CONF_HORIZON, CONF_MIN_ON, CONF_MIN_OFF, CONF_SWITCH_COST, CONF_HEAT_COST,
CONF_BAND, DEFAULTS,
)
from .data_quality import freshness
from .physics import Learner, optimize
from .weather_sources import choose_weather_sources, sources_for, normalize
@@ -61,14 +63,15 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._on_samples = min(m.on_count for m in self.learners.values())
self._off_samples = min(m.off_count for m in self.learners.values())
def _value(self, entity_id: str, low: float, high: float) -> float | None:
def _sensor_quality(self, entity_id: str, now: datetime, limit: float) -> dict:
state = self.hass.states.get(entity_id)
if state is None or state.state in ("unknown", "unavailable"):
return None
value = normalize("temperature", state.state, state.attributes.get("unit_of_measurement"))
if value is None or not low <= value <= high:
return None
return value
if state is None:
return {"status": "missing", "age_seconds": None}
if state.state in ("unknown", "unavailable"):
return {"status": state.state, "age_seconds": None}
# last_updated is a conservative compatibility fallback for older HA.
return freshness(getattr(state, "last_reported", getattr(state, "last_updated", None)),
now, limit * 60)
async def _async_update_data(self) -> dict[str, Any]:
now = datetime.now(timezone.utc)
@@ -83,17 +86,41 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
watched = set(self.rooms) | weather_entity_ids | {
self.config[CONF_CLIMATE], self.config[CONF_HEATER]}
snapshot = {entity: self.hass.states.get(entity) for entity in watched}
weather_states = {
entity_id: (state.state, state.attributes.get("unit_of_measurement"))
for entity_id in weather_entity_ids
if (state := self.hass.states.get(entity_id)) is not None
}
room_quality = {}
temps = {}
for room in self.rooms:
quality = self._sensor_quality(room, now, self.config[CONF_ROOM_MAX_AGE])
value = None
if quality["status"] == "valid":
state = snapshot[room]
value = normalize("temperature", state.state, state.attributes.get("unit_of_measurement"))
if value is None or not 0 <= value <= 45:
quality["status"] = "invalid_value_or_unit"
value = None
room_quality[room] = quality
temps[room] = value
weather_states = {}
weather_quality = {}
for entity_id in weather_entity_ids:
quality = self._sensor_quality(entity_id, now, self.config[CONF_WEATHER_MAX_AGE])
weather_quality[entity_id] = quality
if quality["status"] == "valid":
state = snapshot[entity_id]
weather_states[entity_id] = (state.state, state.attributes.get("unit_of_measurement"))
weather, active_sources, weather_comparisons = choose_weather_sources(
self.config, weather_states
)
metric_quality = {}
for metric in WEATHER_SOURCES:
metric_quality[metric] = {}
for entity_id in sources_for(self.config, metric):
quality = dict(weather_quality[entity_id])
if quality["status"] == "valid" and entity_id not in weather_comparisons[metric]:
quality["status"] = "invalid_value_or_unit"
metric_quality[metric][entity_id] = quality
outside = weather["temperature"]
temps = {room: self._value(room, 0.0, 45.0) for room in self.rooms}
status: dict[str, Any] = {
"input_quality": {"rooms": room_quality, "weather": metric_quality},
"status": "waiting_for_sensors", "proposal": None, "prediction_60m": {},
"temperatures": {r: t for r, t in temps.items() if t is not None},
"on_samples": self._on_samples, "off_samples": self._off_samples,