Treat window states unknown at startup as unknown, and continue across restarts
Reproduced: with a window sensor already unavailable at startup, the forecast assumed the window closed, exactly as for a window last seen closed, and the first reading of an already open window was logged as a confirmed opening. - An opening or closing is confirmed only when witnessed: the previous reading was the other state with no unknown period between. A first reading, or a change across an outage or restart, is unconfirmed. - Last known states, open events and recovery are saved (also whenever a window's state changes) and restored as unknown until the sensor reports; saved state for another sensor or polarity is ignored. Downtime counts as unknown time of an interrupted event. - Forecast assumption per window: observed; last seen open, assumed open only until the configured open time after it was last seen (never renewed); last seen closed or never observed, no ventilation but marked uncertain. Status shows each window's assumption; zone forecasts list their uncertain windows. - Events report a duration only when both ends are confirmed, otherwise a minimum (and a maximum when the opening was seen). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
49565d610a
commit
13cb972566
5 files changed
+353
-55
No files matched your search
@@ -520,7 +520,9 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
details[key] = {"temperature": round(temp, 3),
|
||||
"model_in_use": "extended" if model.uses_extended else "base",
|
||||
"contributions_c_per_h": model.contributions(temp, inputs),
|
||||
"coefficients": coefficients}
|
||||
"coefficients": coefficients,
|
||||
# Windows whose state the forecast had to assume (sensor unknown).
|
||||
"uncertain_windows": list((vent_inputs.get(key) or {}).get("unknown_windows", []))}
|
||||
return details
|
||||
|
||||
def _window_diagnostics(self, now: float, weather: dict[str, Any], temps: dict[str, float | None],
|
||||
@@ -531,11 +533,14 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
is_open, since = info.get("open"), info.get("since")
|
||||
direction = weather.get("wind_direction")
|
||||
last_known, unknown_since = info.get("last_known_open"), info.get("unknown_since")
|
||||
assumed = self._ventilation.assumption(window, float(self.config[CONF_WINDOW_OPEN_MINUTES]), now)
|
||||
windows[window.name] = {
|
||||
"zone": self.zones[window.zone].name, "sensor": window.sensor, "enabled": window.enabled,
|
||||
"state": "unknown" if is_open is None else "open" if is_open else "closed",
|
||||
"last_known_state": None if last_known is None else "open" if last_known else "closed",
|
||||
"unknown_minutes": round((now - unknown_since) / 60, 1) if unknown_since else None,
|
||||
# What forecasts assume: observed, or a bounded guess from the last reading.
|
||||
"forecast_assumption": assumed["basis"], "forecast_open_minutes": assumed["open_minutes"],
|
||||
"open_minutes": round((now - since) / 60, 1) if is_open and since else None,
|
||||
"azimuth": window.azimuth, "opening_factor": window.opening, "inverted": window.inverted,
|
||||
"relative_wind_direction": info.get("relative_wind_direction"),
|
||||
@@ -799,8 +804,10 @@ class HeatingMPCCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.ventilation.get(room))) for room in vals},
|
||||
inputs={room: {"internal": internal[room], "ventilation": vent_inputs.get(room)} for room in vals})
|
||||
status["forecast_accuracy"] = self._evaluator.summary(self.rooms)
|
||||
if learned and not self._stale(run):
|
||||
# Persist at each sample so restart does not erase hours of training.
|
||||
# Persist at each sample so restart does not erase hours of training, and when
|
||||
# a window's state changes so a restart continues from it.
|
||||
window_changed, self._ventilation.changed = self._ventilation.changed, False
|
||||
if (learned or window_changed) and not self._stale(run):
|
||||
await self._async_save(active_sources["temperature"])
|
||||
if self._stale(run):
|
||||
return self._superseded(status)
|
||||
|
||||
Reference in new issue
Block a user