Document the stabilization pass

README: window unknown states, what configuration changes do to the learned
model, inputs_changed tolerances, coefficient confidence, model_changes, and
Model Error vs Forecast Error. docs/MODEL.md: configuration-change table,
confidence semantics, both validation modes. CHANGELOG gains an Unreleased
section (version unchanged); AUDIT and ROADMAP record findings, evidence and
remaining risks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-27 03:55:06 +02:00
1 parent 8eef04c385
commit 35370958fc
5 files changed
+340 -58

No files matched your search

+106 -42
View File
@@ -26,14 +26,24 @@ heat source** and **Add window** create items that can each be edited or deleted
later. **Configure** holds everything else: thermostat, relay, weather sources,
report-age limits, MPC settings and model options.
**Zones** are rooms or areas. Each has a name, one or more temperature sensors with
sensor weights (how much each counts in the zone temperature), a comfort weight
(how much the zone matters to the MPC), an optional target offset relative to the
thermostat's target, and optional adjacent zones it exchanges heat with. Sensor and
comfort weights never change the learned thermal coefficients. A sensor belongs to
one zone only. Upgrading from 0.3 turns each room sensor into a one-sensor zone
with the same comfort weight; to group two sensors of one room, edit one zone to
include both and delete the other.
**Zones** are rooms or areas. Each has a name, one or more temperature sensors, a
comfort weight (how much the zone matters to the MPC), an optional target offset
relative to the thermostat's target, and optional adjacent zones it exchanges heat
with. A zone with several sensors gets a second step, **Sensor weights**: one
numbered field per sensor (listed above the fields), setting how much each counts
in the zone temperature. A sensor belongs to one zone only. Upgrading from 0.3
turns each room sensor into a one-sensor zone with the same comfort weight; to
group two sensors of one room, edit one zone to include both and delete the other.
**What a change does to the learned model.** The zone temperature is defined by
its sensors and the ratio of their weights; changing either recalibrates that
zone (its coefficients are kept as a starting point, but their confidence, sample
counts and accuracy statistics start over, so *Ready* has to be earned again).
Scaling all of a zone's sensor weights by the same factor changes nothing. Adding,
editing or removing a heat source, a neighbour, a window, a window's orientation
or the recovery time resets only the coefficients that depend on it. Comfort
weight, target offset and names never touch the model. Each recalibration is
listed in the Status attribute `model_changes`.
**Heat sources** are equipment whose electricity ends up as heat in a zone, such as
a UPS feeding computers. Settings: an active-power sensor (W or kW; VA and % are
@@ -48,7 +58,11 @@ modeled; the persistent load is already part of the zone's learned behavior.
reports *on* when closed (inverted), the direction the outside of the window faces
(degrees clockwise from north; default 345°, editable) and an opening factor
(1.0 normal, lower for tilted, higher for large). Unknown or unavailable states are
treated as unknown, never as closed.
treated as unknown, never as closed: a window last seen open counts as possibly
open (the forecast assumes it open, the zone's base model does not learn), and an
opening that ended while the sensor was unknown is logged as unconfirmed, with its
longest possible duration. Recovery after closing is learned only from a confirmed
close.
Model options in **Configure**: how fast extra electrical heat is assumed to fade in
forecasts (half-life, default 30 minutes), how long an open window is assumed to
@@ -107,13 +121,16 @@ Entities (entity IDs may vary):
- **Heating MPC Last Sample** — what the last poll did for learning (for example
*Learned*, *Relay switched*, *Waiting for radiators to settle*); its history shows
when learning happens.
- **Heating MPC Forecast Error 15/30/60/120 min** — how far the thermal model's
forecasts were from what actually happened (see *Forecast accuracy*).
- **Heating MPC Model Error 15/30/60/120 min** — how well the thermal model
explains what happened, replayed with the inputs that actually occurred.
- **Heating MPC Forecast Error** — how far published forecasts were from what
happened, using only what was known when they were made (see *Forecast
accuracy*).
## Known limitations / safety
- **Do not** connect the proposed binary sensor directly to the heating switch or build an automation from it. A production controller would need watchdog, manual override, safety handling, and an exclusive relay owner with Generic Thermostat fallback.
- Weather variables other than temperature, wind and gusts are **captured but not fitted**. Wind and gusts are used only for open windows. Keep optional model terms only when **Forecast Error** shows they help (`improvement_vs_base`).
- Weather variables other than temperature, wind and gusts are **captured but not fitted**. Wind and gusts are used only for open windows. Keep optional model terms only when **Model Error** shows they help (`improvement_vs_base`).
- One temperature per zone (no separate wall mass), configured response times, and single-scenario forecasts of electrical heat and windows. See [docs/MODEL.md](docs/MODEL.md#limitations).
- All rooms share one heating actuator. The model does not command individual radiators.
- No weather forecast: assumes outdoor temperature remains constant during simulated horizon.
@@ -142,7 +159,10 @@ with it OFF, which can take a few days depending on how often the boiler runs.
The **Heating MPC Status** entity's state is one of `waiting_for_sensors`,
`no_zones` (add a zone), `learning`, `paused` (thermostat not in `heat`), `waiting_for_target`, `ready`,
`inputs_changed` or `prediction_error`. Proposed heat is available only while it
is `ready`. Useful attributes:
is `ready`. `inputs_changed` means an input moved meaningfully while the plan was
being computed (beyond 0.25 °C, 50 W, 1 m/s wind, 2 m/s gust or 20° direction, or
any change of a window, the relay or the thermostat); `changed_inputs` lists them,
the plan is discarded and the next poll computes a fresh one. Useful attributes:
- `input_quality` — per zone (with each of its sensors) and per weather source
(grouped by metric): `status` (`valid`, `missing`, `unknown`, `unavailable`,
@@ -156,8 +176,10 @@ is `ready`. Useful attributes:
- `heat_sources` — per source: measured and effective power (after removing
included sources), baseline, variable power above it, thermal response, whether it
has settled.
- `windows` — per window: state, minutes open, wind direction relative to the
window's facing and the resulting exposure (1 = wind straight at it).
- `windows` — per window: state (`open`, `closed` or `unknown`), the last known
state and for how long the sensor has been unknown, minutes open, wind direction
relative to the window's facing and the resulting exposure (1 = wind straight at
it).
`ventilation` — per zone: current opening, recovery, temperature before opening,
its contribution in °C/h and the ventilation coefficients. `ventilation_events` —
the last five openings with temperatures before, lowest, at closing and 30
@@ -170,14 +192,20 @@ is `ready`. Useful attributes:
`ventilation` (only window-affected zones; given to the ventilation model),
`partly_rejected` (an outlier in some zone), `interval_out_of_range`,
`relay_changed`, `outside_source_changed`, `lag_uncertain` or `waiting_for_inputs`.
- `models` — per-zone base coefficients, accepted sample counts and the in-sample
5-minute error (`mae_5m`, **not** a forecast accuracy), `model_in_use` and the
extended layer's coefficients with confidence.
- `models` — per-zone base coefficients with their `confidence`, accepted sample
counts and the in-sample 5-minute error (`mae_5m`, **not** a forecast accuracy),
`model_in_use` and the extended layer's coefficients with confidence. Confidence
(0 to 1) is the share of a coefficient's information that comes from data rather
than the starting assumption. It grows only while the data varies that
coefficient's input: weeks with the relay always off leave `heat` at 0, however
many samples there are.
- `model_changes` — the last recalibrations: when, which zone, which coefficients
were reset and why.
- `retired_zone_models` — removed zones whose learned models are kept.
`input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation`
and `ventilation_events` change every poll, so they are not written to the
Recorder; the other attributes are.
`input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation`,
`ventilation_events` and `model_changes` are not written to the Recorder; the other
attributes are.
### Choosing freshness limits
@@ -216,22 +244,35 @@ age limit.
## Forecast accuracy
The four **Forecast Error** sensors measure the thermal model against reality,
continuously and out of sample. At every poll the integration records each zone's
reading, the outdoor temperature, the radiator-lag estimate and the model's
parameters. 15, 30, 60 and 120 minutes later it replays the zones from the earlier
reading using the parameters it had **then**, the outdoor temperature held at its
starting value, and the radiator heat that actually followed from the relay's real
ON/OFF history. The result is compared with the current reading.
Two different questions, two kinds of sensor:
This tests the thermal model, not the MPC plan, because the plan is never
executed. All zones are replayed together, with the electrical heat and window
openings that actually happened, and the base model is scored on the same spans. Each sensor's state is the mean absolute error (°C) over the last 7 days,
across all zones. Attributes, all in °C except counts:
- **Model Error** (four sensors, 15/30/60/120 min): *does the thermal model explain
what happened?* Replayed with the relay, electrical heat and windows that
actually followed. Useful for judging the model and its options. It is **not**
a forecast accuracy: a real forecast does not know when the relay will switch or
a window will open.
- **Forecast Error** (one sensor): *how good were the forecasts as published?*
Uses only what was known when each forecast was made.
- `samples` — number of scored forecasts. Consecutive forecasts overlap, so they
are not independent; treat small counts as indicative only.
- `bias` — mean error: negative means the model predicts too cold.
### Model Error
At every poll the integration records each zone's reading, the outdoor
temperature, the radiator-lag estimate and the model's parameters. 15, 30, 60 and
120 minutes later it replays all zones together from that reading using the
parameters it had **then**, the outdoor temperature held at its starting value,
and the relay, electrical heat and window history that actually followed. The base
model is scored on the same spans. Each sensor's state is the mean absolute error
(°C) over the last 7 days, across all zones. Attributes, all in °C except counts:
- `samples` — number of scored forecasts; `effective_samples` — roughly
independent ones. Forecasts start every 5 minutes, so 60-minute forecasts
overlap twelvefold, and zones share one boiler, so `effective_samples` is
`samples × 5 / horizon / zones`.
- `quality` — `insufficient` below 5 effective samples (the state then stays
unknown; the numbers are still in the attributes), `indicative` below 30, `ok`
from 30.
- `mae`, `rmse`, `bias` — mean absolute, root-mean-square and mean error
(negative bias: the model predicts too cold).
- `persistence_mae` — error of simply assuming no change; the baseline to beat.
- `skill` — `1 - mae / persistence_mae`: 1 is perfect, 0 is no better than
assuming no change, negative is worse. Empty while the temperature did not change.
@@ -240,13 +281,35 @@ across all zones. Attributes, all in °C except counts:
make it worse: switch that option off).
- `zones` — the same numbers per zone (not written to the Recorder).
Forecasts are scored only where the inputs were known: polls 4.5–5.5 minutes
apart, a usable reading at the start and the end, and a well-known radiator-lag
estimate throughout. A missing relay, outdoor temperature or all zones restarts the
replay window, so scores resume 15–120 minutes later. Statistics are kept with the
model and survive restarts; deleting the model resets them.
Spans are scored only where the inputs were known: polls 4.5–5.5 minutes apart, a
usable reading at the start and the end, and a well-known radiator-lag estimate
throughout. A missing relay, outdoor temperature or all zones restarts the replay
window, so scores resume 15–120 minutes later. Statistics are kept with the model
and survive restarts; recalibrating a zone or deleting the model resets them.
What it does not cover: outdoor temperature changes during the horizon (held
### Forecast Error
Each time a plan is published, its forecast is recorded as it was: start
temperatures, the trajectory, the plan, the model revision and the assumptions
(outdoor temperature and wind held, electrical heat fading, open windows closing).
When the forecast's horizon comes, it is scored against the reading, but only if
the relay actually followed the plan: in every 15-minute block up to that horizon
the relay must have been in the planned state for at least 12 of the 15 minutes.
Otherwise the forecast is not an error measurement, and is counted instead:
- `counterfactual` — the relay did something else: Generic Thermostat, which
controls it, disagreed with the plan. Expected in observation mode whenever the
two disagree.
- `unverifiable` — the relay history is incomplete for that span.
- `missing` — no poll near the due time, or no reading for a zone.
The state is the 60-minute MAE once its quality is at least `indicative`; the
attribute `horizons` (not written to the Recorder) gives MAE, RMSE, bias, the
no-change error, `improvement_vs_persistence`, sample counts, quality, the counts
above and the per-zone figures for each horizon. Statistics cover 7 days and
survive restarts; forecasts not yet due when HA restarts are dropped.
What neither covers: outdoor temperature changes during the horizon (held
constant, as in the MPC), and periods when the model was still learning (included,
so early numbers are pessimistic). The roadmap's working target is about
0.3–0.4 °C at 60 minutes with clearly positive skill, before any control is
@@ -287,7 +350,8 @@ The brand icons in `custom_components/heating_mpc/brand/` are drawn by
`python scripts/make_brand_icons.py` (uses Pillow from the test requirements);
the package check verifies their sizes and transparency.
HA tests cover setup, config/options forms, migration, reload and entity cleanup,
HA tests cover setup, config/options forms, migration, reload (a single relay
listener and polling coordinator after repeated reloads) and entity cleanup,
scheduled sampling and poll cancellation, the relay listener, stored model
restoration, retention and deletion, missing inputs, manual off/presets, optimizer
failure and Recorder exclusions. A fixture rejects all HA service calls throughout