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:
1 parent
8eef04c385
commit
35370958fc
5 files changed
+340
-58
No files matched your search
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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):
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user