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

+77
View File
@@ -320,3 +320,80 @@ weather forecasts. Learners of removed zones keep only the base model.
**Verification.** `sh scripts/check.sh` passes in both environments (lint, 125 **Verification.** `sh scripts/check.sh` passes in both environments (lint, 125
offline tests, 26 HA Core tests, compilation, package/translation/changelog checks). offline tests, 26 HA Core tests, compilation, package/translation/changelog checks).
The offline tests also pass on Python 3.9.6. The offline tests also pass on Python 3.9.6.
## Stabilization after 0.4.0 — 2026-10-27
Scope: correctness of 0.4.0, no new features, version unchanged. Repository code
and tests only (HA Core 2025.12.0 and 2026.2.3 harness); no HAOS, device or
deployment testing. Still no actuator code; the static and runtime service-call
guards pass.
Confirmed issues and fixes:
1. **Unknown window states read as closed** (2bddc74). Reproduced: an open window
whose sensor became unavailable ended the ventilation event, started recovery
at full weight and let the forecast assume a closed window. Windows are now
tri-state with a remembered last known state: last seen open → possibly open
(base and extended layers do not learn, the forecast assumes open); a close
while unknown is logged as unconfirmed with its longest possible duration;
recovery is learned only after a confirmed close. Seven transition tests
(open/closed/unknown in every order, restart while unknown, polarity).
2. **Learned coefficients reused after incompatible changes** (099c63c, 8eef04c).
By construction: changing a zone's sensors or weight ratio, a heat source, a
window or the recovery time kept every coefficient. New
`compatibility.py` fingerprints each component's configuration; on restore
only changed components are reset (a measurement change recalibrates the zone:
coefficients kept as a starting point, uncertainty, counts and accuracy
statistics reset). Comfort weight, target offset and names never invalidate.
Heat-source baseline samples are dropped when the effective power's definition
changes. 0.4.0 data has no fingerprints, is assumed compatible, and is
fingerprinted at the first start. Resets are logged in `model_changes`.
3. **Plans published from changed inputs** (d73e32a). The pre/post-optimization
check covered only temperatures, relay and thermostat, and an overtaken update
could still publish. The input snapshot now covers zone and outdoor
temperatures, electrical power, wind, windows, relay and thermostat, with
tolerances so sensor noise does not discard every plan; a run counter keeps an
older run from overwriting a newer result; shutdown is checked after saving
and after the worker; a per-entry storage lock serializes save, restore and
delete (also fixing a delete-during-save race that could recreate the file).
4. **Model accuracy reported as forecast accuracy** (26c415f). The Forecast Error
sensors replayed the model with the actual future relay, electrical heat and
windows. They are now **Model Error** (unique IDs unchanged) with RMSE,
effective samples and a quality label, and their state stays unknown below 5
effective samples. A new **Forecast Error** sensor scores published forecasts
with the plan and assumptions of the moment, only when the relay followed the
plan (≥ 12 of every 15 minutes in the planned state); otherwise the forecast is
counted as counterfactual, unverifiable or missing, never as error.
Also reviewed:
- **Confidence** (a44163a). Extended and ventilation coefficients already used
posterior variance. Base coefficients now report it too; a test shows two weeks
of non-exciting data (relay off, steady difference) keep `heat` at 0 and `loss`
and `bias` below 0.8, while four varied days bring all three above 0.8.
Readiness still needs sample counts; it is a gate, not a confidence.
- **Lifecycle** (4911b32). Three reloads leave one relay listener and one polling
coordinator; after unload nothing is recorded or published. The test was
checked to fail with the unsubscribe removed.
- **Sensor weights UI** (9bb64e7, user report). Per-sensor weights existed but
were an unlabelled second step; they are now numbered fields with the sensors
listed, in English and Romanian.
Remaining risks:
- In observation mode the relay follows the MPC plan mainly when Generic
Thermostat happens to agree, so Forecast Error may stay `insufficient` for a
long time, and the scored subset is biased towards those periods.
- `effective_samples` corrects for overlapping horizons and shared zones only; it
is not an autocorrelation estimate. Quality thresholds (5, 30) are judgement.
- Configuration changes made before this update cannot be detected: 0.4.0 models
are fingerprinted as they are. A sensor replaced under the same entity ID, or a
weather provider swap with a systematic offset, does not change any fingerprint.
- Input-change tolerances are fixed constants, not options.
- Published forecasts awaiting their horizon are lost on restart.
- None of this has run on HAOS or with real devices.
**Verification.** In both environments (HA 2025.12.0 and 2026.2.3, Python 3.13):
ruff, 159 offline tests, 28 HA Core tests; the offline tests also pass on Python
3.9.6. `sh scripts/check.sh` additionally validates packaging, translations and
the changelog.
+75
View File
@@ -2,6 +2,81 @@
Engineering findings and test evidence behind each change are in [AUDIT.md](AUDIT.md). Engineering findings and test evidence behind each change are in [AUDIT.md](AUDIT.md).
## Unreleased — stability and correctness
Fixes four correctness problems found in 0.4.0 and separates model accuracy from
forecast accuracy. No new product features; the version stays 0.4.0 until
released. Still **observation-only**. Tested in the Home Assistant Core harness
with HA 2025.12.0 and 2026.2.3; not yet on HAOS.
### Upgrade notes
- **Forecast Error 15/30/60/120 min are renamed Model Error.** Unique IDs and,
on existing installs, entity IDs are unchanged, so history continues. Their
state now stays *unknown* until there are at least 5 roughly independent spans
(`quality` is `insufficient`); the numbers stay in the attributes meanwhile.
- **A new Heating MPC Forecast Error sensor** scores forecasts as they were
published. It stays unknown until the relay has followed enough MPC plans; while
Generic Thermostat disagrees with the plan, most forecasts are counted as
`counterfactual` instead of being scored.
- On the first start, the saved model gets configuration fingerprints. Models
learned under 0.4.0 are assumed to match the configuration they are loaded with;
later changes are checked against these fingerprints.
- Zone forecast attribute `coefficients.base` now gives `value` and `confidence`
per coefficient instead of a bare value.
- Downgrading to 0.4.0 keeps the learned models but drops the Model Error
statistics (0.4.0 rejects their new format).
### Fixed
- **Unknown window states no longer pass as closed.** An unavailable window
sensor used to end an open-window event, start recovery and let the forecast
assume a closed window. Now the last known state is kept: a window last seen
open is treated as possibly open (no base learning, forecast assumes open), an
event closed while the sensor was unknown is marked unconfirmed with a bounded
duration, and recovery is learned only after a confirmed close.
- **Learned parameters are no longer reused after incompatible changes.** Each
learned component has a fingerprint of the configuration it was learned under
(see *Persisted data*). Changing a zone's sensors or the ratio of their weights
recalibrates that zone; changing a heat source, neighbour, window, orientation
or recovery time resets only the affected coefficients; comfort weight, target
offset and names never invalidate anything. Every recalibration is listed in the
Status attribute `model_changes`.
- **Plans are no longer published from inputs that changed meanwhile.** Before
and after the plan is computed, the integration compares every input it used
(zone and outdoor temperatures beyond 0.25 °C, power beyond 50 W, wind, window
states, relay and thermostat) and discards the plan if one changed meaningfully
(`inputs_changed`, with `changed_inputs`). An older run finishing late never
overwrites a newer result, nothing is published after unload, and saving and
deleting the model can no longer overlap.
### Changed
- **Model accuracy and forecast accuracy are reported separately.** Model Error
replays the model with the inputs that actually happened (relay, electrical
heat, windows); Forecast Error uses only what was known when the forecast was
published, and scores it only when the relay followed the forecast's plan
(in the planned state for at least 12 minutes of every 15-minute block). Both report MAE, RMSE, bias, the
no-change baseline, the improvement over it, sample counts, effective
(roughly independent) sample counts and a quality label.
- Base-model coefficients (`loss`, `heat`, `bias`) report a confidence from their
posterior variance, like the other layers: it reflects whether the data varied
their inputs, not how many samples there are.
- The zone form's sensor weights are numbered fields with the sensors listed
above them (English and Romanian), and the zone form explains the two kinds of
weight.
### Persisted data
- `schema_version` stays 2. New optional keys: `fingerprint_version`,
`fingerprints`, `model_changes` (last 20), `operational_evaluation` (Forecast
Error statistics, 7 days) and per-source baseline fingerprints in
`heat_sources`. Retired zone models keep their fingerprint.
- Model Error statistics gain RMSE columns; 0.3 and 0.4.0 statistics load (their
RMSE stays empty until new forecasts are scored).
- Pending published forecasts are kept in memory only; a restart drops those not
yet due.
## 0.4.0 — 2026-10-27 ## 0.4.0 — 2026-10-27
Thermal zones, electrical heat sources, windows with ventilation and wind, and Thermal zones, electrical heat sources, windows with ventilation and wind, and
+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, later. **Configure** holds everything else: thermostat, relay, weather sources,
report-age limits, MPC settings and model options. report-age limits, MPC settings and model options.
**Zones** are rooms or areas. Each has a name, one or more temperature sensors with **Zones** are rooms or areas. Each has a name, one or more temperature sensors, a
sensor weights (how much each counts in the zone temperature), a comfort weight comfort weight (how much the zone matters to the MPC), an optional target offset
(how much the zone matters to the MPC), an optional target offset relative to the relative to the thermostat's target, and optional adjacent zones it exchanges heat
thermostat's target, and optional adjacent zones it exchanges heat with. Sensor and with. A zone with several sensors gets a second step, **Sensor weights**: one
comfort weights never change the learned thermal coefficients. A sensor belongs to numbered field per sensor (listed above the fields), setting how much each counts
one zone only. Upgrading from 0.3 turns each room sensor into a one-sensor zone in the zone temperature. A sensor belongs to one zone only. Upgrading from 0.3
with the same comfort weight; to group two sensors of one room, edit one zone to turns each room sensor into a one-sensor zone with the same comfort weight; to
include both and delete the other. 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 **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 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 reports *on* when closed (inverted), the direction the outside of the window faces
(degrees clockwise from north; default 345°, editable) and an opening factor (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 (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 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 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 - **Heating MPC Last Sample** — what the last poll did for learning (for example
*Learned*, *Relay switched*, *Waiting for radiators to settle*); its history shows *Learned*, *Relay switched*, *Waiting for radiators to settle*); its history shows
when learning happens. when learning happens.
- **Heating MPC Forecast Error 15/30/60/120 min** — how far the thermal model's - **Heating MPC Model Error 15/30/60/120 min** — how well the thermal model
forecasts were from what actually happened (see *Forecast accuracy*). 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 ## 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. - **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). - 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. - All rooms share one heating actuator. The model does not command individual radiators.
- No weather forecast: assumes outdoor temperature remains constant during simulated horizon. - 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`, 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`, `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 `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 - `input_quality` — per zone (with each of its sensors) and per weather source
(grouped by metric): `status` (`valid`, `missing`, `unknown`, `unavailable`, (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 - `heat_sources` — per source: measured and effective power (after removing
included sources), baseline, variable power above it, thermal response, whether it included sources), baseline, variable power above it, thermal response, whether it
has settled. has settled.
- `windows` — per window: state, minutes open, wind direction relative to the - `windows` — per window: state (`open`, `closed` or `unknown`), the last known
window's facing and the resulting exposure (1 = wind straight at it). 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, `ventilation` — per zone: current opening, recovery, temperature before opening,
its contribution in °C/h and the ventilation coefficients. `ventilation_events` — its contribution in °C/h and the ventilation coefficients. `ventilation_events` —
the last five openings with temperatures before, lowest, at closing and 30 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), `ventilation` (only window-affected zones; given to the ventilation model),
`partly_rejected` (an outlier in some zone), `interval_out_of_range`, `partly_rejected` (an outlier in some zone), `interval_out_of_range`,
`relay_changed`, `outside_source_changed`, `lag_uncertain` or `waiting_for_inputs`. `relay_changed`, `outside_source_changed`, `lag_uncertain` or `waiting_for_inputs`.
- `models` — per-zone base coefficients, accepted sample counts and the in-sample - `models` — per-zone base coefficients with their `confidence`, accepted sample
5-minute error (`mae_5m`, **not** a forecast accuracy), `model_in_use` and the counts and the in-sample 5-minute error (`mae_5m`, **not** a forecast accuracy),
extended layer's coefficients with confidence. `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. - `retired_zone_models` — removed zones whose learned models are kept.
`input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation` `input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation`,
and `ventilation_events` change every poll, so they are not written to the `ventilation_events` and `model_changes` are not written to the Recorder; the other
Recorder; the other attributes are. attributes are.
### Choosing freshness limits ### Choosing freshness limits
@@ -216,22 +244,35 @@ age limit.
## Forecast accuracy ## Forecast accuracy
The four **Forecast Error** sensors measure the thermal model against reality, Two different questions, two kinds of sensor:
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.
This tests the thermal model, not the MPC plan, because the plan is never - **Model Error** (four sensors, 15/30/60/120 min): *does the thermal model explain
executed. All zones are replayed together, with the electrical heat and window what happened?* Replayed with the relay, electrical heat and windows that
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, actually followed. Useful for judging the model and its options. It is **not**
across all zones. Attributes, all in °C except counts: 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 ### Model Error
are not independent; treat small counts as indicative only.
- `bias` — mean error: negative means the model predicts too cold. 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. - `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 - `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. 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). make it worse: switch that option off).
- `zones` — the same numbers per zone (not written to the Recorder). - `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 Spans are scored only where the inputs were known: polls 4.5–5.5 minutes apart, a
apart, a usable reading at the start and the end, and a well-known radiator-lag usable reading at the start and the end, and a well-known radiator-lag estimate
estimate throughout. A missing relay, outdoor temperature or all zones restarts the throughout. A missing relay, outdoor temperature or all zones restarts the replay
replay window, so scores resume 15–120 minutes later. Statistics are kept with the window, so scores resume 15–120 minutes later. Statistics are kept with the model
model and survive restarts; deleting the model resets them. 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, 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 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 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); `python scripts/make_brand_icons.py` (uses Pillow from the test requirements);
the package check verifies their sizes and transparency. 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 scheduled sampling and poll cancellation, the relay listener, stored model
restoration, retention and deletion, missing inputs, manual off/presets, optimizer restoration, retention and deletion, missing inputs, manual off/presets, optimizer
failure and Recorder exclusions. A fixture rejects all HA service calls throughout failure and Recorder exclusions. A fixture rejects all HA service calls throughout
+18 -5
View File
@@ -85,6 +85,16 @@ Release 0.2.2 adds that event-based relay history (read-only listener), keeps
models of rooms removed in Options, shows each sensor's longest report gap, stops models of rooms removed in Options, shows each sensor's longest report gap, stops
recording volatile attributes, and tests against HA 2025.12.0 and 2026.2.3. recording volatile attributes, and tests against HA 2025.12.0 and 2026.2.3.
### Stabilization after 0.4.0 — 2026-10-27
See AUDIT.md. Four confirmed problems fixed, each with regression tests: unknown
window states read as closed; learned coefficients reused after incompatible
configuration changes (now per-component fingerprints with selective
recalibration, logged in `model_changes`); plans published from inputs that
changed during optimization or by an overtaken run; and model accuracy reported
as if it were forecast accuracy. Reload/unload tests now cover repeated reloads
(one relay listener, one polling coordinator). Not yet released or tested on HAOS.
## Milestone 1 — Explainable forecasts and dashboards (target v0.3.0) ## Milestone 1 — Explainable forecasts and dashboards (target v0.3.0)
**Objective:** Users can verify what the model predicts and whether it is actually accurate. **Objective:** Users can verify what the model predicts and whether it is actually accurate.
@@ -92,9 +102,9 @@ recording volatile attributes, and tests against HA 2025.12.0 and 2026.2.3.
- [ ] Expose trajectories for each room at 5–15-minute resolution for 2–3 hours, with forecast generation timestamp, horizon and input provenance. *(0.3.0: 15-minute trajectory over the horizon; generation timestamp and provenance still to add.)* - [ ] Expose trajectories for each room at 5–15-minute resolution for 2–3 hours, with forecast generation timestamp, horizon and input provenance. *(0.3.0: 15-minute trajectory over the horizon; generation timestamp and provenance still to add.)*
- [x] Expose predicted +15/+30/+60/+120 minute temperatures and full proposed ON/OFF schedule. *(0.3.0: `forecast_15min` and `plan_15min` on each room forecast entity.)* - [x] Expose predicted +15/+30/+60/+120 minute temperatures and full proposed ON/OFF schedule. *(0.3.0: `forecast_15min` and `plan_15min` on each room forecast entity.)*
- [ ] Provide an HA dashboard example using native cards or a clearly documented optional chart card (do not bundle a heavyweight frontend unnecessarily). - [ ] Provide an HA dashboard example using native cards or a clearly documented optional chart card (do not bundle a heavyweight frontend unnecessarily).
- [ ] Make clear that the **MPC plan forecast is counterfactual** while Generic Thermostat is controlling the relay; add a separate actual-policy forecast/evaluation pathway. *(0.3.0: plan forecasts labelled counterfactual; evaluation uses the actual relay history. An actual-policy forecast, simulating Generic Thermostat, is still open.)* - [ ] Make clear that the **MPC plan forecast is counterfactual** while Generic Thermostat is controlling the relay; add a separate actual-policy forecast/evaluation pathway. *(0.3.0: plan forecasts labelled counterfactual; evaluation uses the actual relay history. Stabilization: published forecasts are scored only when the relay followed their plan and otherwise counted as `counterfactual`. An actual-policy forecast, simulating Generic Thermostat, is still open.)*
- [ ] Store a bounded history of predictions with snapshots of inputs, proposed plan and eventually observed outcome. - [ ] Store a bounded history of predictions with snapshots of inputs, proposed plan and eventually observed outcome. *(Stabilization: each published forecast is kept with its start readings, plan, model revision and assumptions until scored, in memory only; the scored statistics persist. No browsable history yet.)*
- [x] Implement out-of-sample/walk-forward MAE and bias at 15, 30, 60 and 120 minutes; include sample counts and confidence/quality labels. *(0.3.0: Forecast Error sensors; replay with parameters frozen at the forecast start and the actual relay history; `skill` against persistence. No confidence intervals yet.)* - [x] Implement out-of-sample/walk-forward MAE and bias at 15, 30, 60 and 120 minutes; include sample counts and confidence/quality labels. *(0.3.0: replay with parameters frozen at the forecast start and the actual relay history; `skill` against persistence. Stabilization: these are now the Model Error sensors, with RMSE, effective sample counts and quality labels; a separate Forecast Error sensor scores published forecasts. No confidence intervals yet.)*
- [ ] Compare against simple benchmarks such as current-temperature persistence and a fixed thermal baseline. *(0.3.0: persistence done; fixed baseline open.)* - [ ] Compare against simple benchmarks such as current-temperature persistence and a fixed thermal baseline. *(0.3.0: persistence done; fixed baseline open.)*
- [ ] Add inspectable explanations: expected heat loss, expected heating response, target, room weights and reason for the next proposed action. - [ ] Add inspectable explanations: expected heat loss, expected heating response, target, room weights and reason for the next proposed action.
@@ -175,8 +185,11 @@ Items 1–3 of the original backlog are done (see AUDIT.md and CHANGELOG.md 0.2.
control, decide a minimum `weight_coverage` and the outdoor-temperature outage control, decide a minimum `weight_coverage` and the outdoor-temperature outage
policy, which still pauses. policy, which still pauses.
3. Configure zones, the UPS heat source and the windows on HAOS; after a week of 3. Configure zones, the UPS heat source and the windows on HAOS; after a week of
heating review `improvement_vs_base`, per-zone bias and which coefficients are heating review Model Error `improvement_vs_base` and `quality`, per-zone bias,
identified. Switch off optional terms that do not help. coefficient `confidence` (base layer included), `model_changes` after any
reconfiguration, and the Forecast Error counts (how many forecasts were scored
vs counterfactual). Switch off optional terms that do not help. Also try an
unavailable window sensor and a zone edit during HAOS acceptance.
4. Milestone 1 (rest): a dashboard example; forecast timestamps/provenance; 4. Milestone 1 (rest): a dashboard example; forecast timestamps/provenance;
explanations for the proposed action; an actual-policy forecast that simulates explanations for the proposed action; an actual-policy forecast that simulates
Generic Thermostat. Generic Thermostat.
+64 -11
View File
@@ -93,9 +93,16 @@ measure the input side or lower the heat fraction accordingly.
Present when a zone has an enabled window. Learned separately, from the part of Present when a zone has an enabled window. Learned separately, from the part of
the measured change the closed-window layers cannot explain, only while a window is the measured change the closed-window layers cannot explain, only while a window is
open, its state is unknown, or the zone is recovering. During those periods the open or the zone is recovering, and only while every window state in the zone is
base and extended layers do **not** learn, so window events cannot inflate `loss` known. While a window is open, possibly open (its sensor is unknown and it was last
or shift `bias`. seen open) or the zone is recovering, the base and extended layers do **not**
learn, so window events cannot inflate `loss` or shift `bias`.
An unknown window state is never read as closed. A window last seen open stays
assumed open in forecasts; one last seen closed contributes no ventilation term.
If a window closes while its sensor was unknown, the event's duration is reported
as a range and the recovery term is not learned from it, because the closing time
is not known.
`vent_wind_dir` is bounded by `vent_wind` (|vent_wind_dir| ≤ vent_wind), so the `vent_wind_dir` is bounded by `vent_wind` (|vent_wind_dir| ≤ vent_wind), so the
combined wind term never goes negative: a leeward window still ventilates. combined wind term never goes negative: a leeward window still ventilates.
@@ -128,11 +135,20 @@ outlier rejection above 0.8 °C per 5 minutes.
Each coefficient reports: Each coefficient reports:
* `confidence`: the share of its information that comes from data rather than the * `confidence`: the share of its information that comes from data rather than the
prior (0 = prior only); prior (0 = prior only), from its posterior variance. It is not a sample count:
it grows only while the data varies the coefficient's input independently of
the others, and with forgetting it falls again during long periods without such
variation. The base layer reports it too (`loss`, `heat`, `bias`);
* `std`: its standard error, using a running estimate of the unexplained noise; * `std`: its standard error, using a running estimate of the unexplained noise;
* `samples`: how many accepted samples actually exercised it; * `samples`: how many accepted samples actually exercised it;
* `identified`: at least 30 exercising samples and confidence ≥ 0.8. * `identified`: at least 30 exercising samples and confidence ≥ 0.8.
The base layer has no per-coefficient sample counts or standard errors; its
`confidence` alone shows identification. In the tests, two weeks of samples with
the relay always off and a steady indoor-outdoor difference leave `heat` at 0 and
`loss` and `bias` below 0.8, while four days with the relay cycling and the
weather changing bring all three above 0.8.
A coefficient can move without being identified. Example from the tests: two rooms A coefficient can move without being identified. Example from the tests: two rooms
whose temperature difference stays fairly steady cannot separate exchange between whose temperature difference stays fairly steady cannot separate exchange between
them from each room's bias; the learner finds a positive coupling but correctly them from each room's bias; the learner finds a positive coupling but correctly
@@ -154,6 +170,27 @@ A zone learns from the interval between two polls only when:
No experiments are run and the boiler is never switched to gather data: everything No experiments are run and the boiler is never switched to gather data: everything
is learned passively. is learned passively.
## Configuration changes
Every learned component stores a fingerprint of the configuration it was learned
under (`custom_components/heating_mpc/compatibility.py`). At startup, components
whose fingerprint changed are reset; the rest are kept.
| Change | Effect |
|---|---|
| A zone's sensors, or the ratio of their weights | Zone recalibrated: coefficients kept as a starting point; uncertainty, counts and accuracy statistics reset. Its extended and ventilation layers start over too (their loss, heat and bias values kept as a starting point) |
| Scaling all of a zone's weights equally | Nothing (the normalized measurement is unchanged) |
| A heat source's sensor, heat share, baseline, response time or "included in" | `internal` of its zone; the source's baseline samples if its effective power changed |
| A neighbour's sensors or weights | `couple:<neighbour>` |
| A window's sensor, polarity or opening factor | that zone's ventilation terms; `vent_cross` of the other zones |
| A window's facing direction | `vent_wind_dir` |
| Recovery time constant | `recovery` |
| Comfort weight, target offset, names | Nothing (not physical) |
Models saved before fingerprints existed (0.4.0) are assumed to match the
configuration they are loaded with, and are fingerprinted at once. Every reset is
logged in `model_changes` with its reason.
## Forecasts ## Forecasts
Forecasts simulate all zones together, step by step, over the MPC horizon (60–180 Forecasts simulate all zones together, step by step, over the MPC horizon (60–180
@@ -177,18 +214,34 @@ they describe what would happen under the MPC's proposed plan.
## Validation ## Validation
The Forecast Error sensors score forecasts that are valid for comparison with Two separate measurements, because they answer different questions.
measured temperatures. At every poll the model parameters are frozen; 15, 30, 60
and 120 minutes later all zones are replayed from that poll using the relay,
electrical heat and window history that actually followed (outdoor temperature held
at its start value), and compared with the readings. The base layer and a
no-change forecast are scored on exactly the same spans:
* `mae`, `bias`: the full model's error; **Model accuracy (Model Error sensors)** scores the model, not a forecast. At
every poll the model parameters are frozen; 15, 30, 60 and 120 minutes later all
zones are replayed from that poll using the relay, electrical heat and window
history that actually followed (outdoor temperature held at its start value),
and compared with the readings. The base layer and a no-change forecast are
scored on exactly the same spans:
* `mae`, `rmse`, `bias`: the full model's error;
* `persistence_mae`, `skill`: against assuming no change; * `persistence_mae`, `skill`: against assuming no change;
* `base_model_mae`, `improvement_vs_base`: against the base layer alone. A feature * `base_model_mae`, `improvement_vs_base`: against the base layer alone. A feature
that does not improve this should be switched off (for example the gust term). that does not improve this should be switched off (for example the gust term).
**Forecast accuracy (Forecast Error sensor)** scores forecasts as published: the
start readings, trajectory, plan, model revision and assumptions of the moment
they were made, nothing replaced with hindsight. Because the plan is the MPC's
proposal while Generic Thermostat controls the relay, a forecast is scored at a
horizon only if the relay was in the planned state for at least 12 minutes of
every 15-minute block up to it; otherwise it is counted as `counterfactual` (or
`unverifiable` when the relay history is incomplete) and never as an error.
Both report how much evidence there is. Forecasts start every 5 minutes, so at
horizon h consecutive forecasts share most of their span, and all zones share one
boiler: `effective_samples = samples × 5 / h / zones`. Below 5 the quality is
`insufficient` and the sensor's state stays unknown; below 30 it is
`indicative`.
Results on synthetic apartments (the tests; these check the method, not real-world Results on synthetic apartments (the tests; these check the method, not real-world
accuracy): accuracy):