Release 0.4.0
Tests / tests (requirements-test-latest.txt) (push) Successful in 1m45s
Tests / tests (requirements-test.txt) (push) Successful in 1m42s

Zones, electrical heat sources, windows with ventilation, wind exposure
and recovery, and exchange between adjacent zones, all observational.
docs/MODEL.md documents the equations, units, coefficient meanings,
priors and bounds, learning conditions, forecast assumptions, validation
method and results, and limitations. README gains a configuration guide
for zones, heat sources and windows, the new diagnostics and the
accuracy comparison against the base model. CHANGELOG has upgrade notes
(config entry version 3, rollback needs the backup, renamed Status
attributes). ROADMAP marks the Milestone 2 items now covered; AUDIT
records the design decisions and evidence. AGENTS.md lists the new
modules and the convention that new model terms start at zero and must
beat the base model out of sample before being relied on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-27 03:02:21 +02:00
1 parent dc92e2bac5
commit 17dbe76223
7 files changed
+491 -66

No files matched your search

+18 -6
View File
@@ -83,6 +83,7 @@ Favor the standard library and small dependencies. Heavy compute must not block
- Validate model identifiability, parameter stability, outlier rejection, missing observations, power outages, heater transitions within a sampling window, and multiple sensors in the same physical room. - Validate model identifiability, parameter stability, outlier rejection, missing observations, power outages, heater transitions within a sampling window, and multiple sensors in the same physical room.
- Weather covariates beyond temperature should be added only if they improve held-out performance; avoid treating `feels_like` as physical outdoor air temperature. Forecast values and observed values must be distinguished. - Weather covariates beyond temperature should be added only if they improve held-out performance; avoid treating `feels_like` as physical outdoor air temperature. Forecast values and observed values must be distinguished.
- One switch heats all rooms: optimization can trade off room comfort, but cannot independently heat a cold bathroom without influencing warmer rooms. - One switch heats all rooms: optimization can trade off room comfort, but cannot independently heat a cold bathroom without influencing warmer rooms.
- Keep the base 3-parameter model as fallback and comparison. New model terms start at a zero prior, are physically bounded, report confidence, learn only when their inputs are known, and must show `improvement_vs_base` in walk-forward replays before being relied on (see docs/MODEL.md).
## Repository conventions ## Repository conventions
@@ -90,22 +91,33 @@ Expected structure (adapt only through deliberate refactoring):
```text ```text
custom_components/heating_mpc/ custom_components/heating_mpc/
__init__.py __init__.py setup, unload, migration
manifest.json manifest.json
config_flow.py config_flow.py config, options and subentry flows (zones, heat sources, windows)
const.py const.py
coordinator.py coordinator.py HA state acquisition, learning schedule, diagnostics
physics.py data_quality.py freshness checks
weather_sources.py weather_sources.py source priorities and unit normalization
zones.py zone parsing and sensor aggregation (pure)
heat_sources.py electrical heat: baseline, response, overlap (pure)
windows.py windows, wind geometry, ventilation, events (pure)
learning.py named-feature RLS (pure)
physics.py base model, radiator lag, optimizer (pure)
zone_model.py layered zone model, compiled forecast step (pure)
evaluation.py walk-forward forecast scoring (pure)
sensor.py sensor.py
binary_sensor.py binary_sensor.py
strings.json strings.json
translations/{en,ro}.json translations/{en,ro}.json
brand/ integration icons (scripts/make_brand_icons.py)
docs/MODEL.md model equations, units, learning conditions, validation
hacs.json hacs.json
tests/ tests/ offline tests; tests/ha for the HA Core harness
README.md README.md
CHANGELOG.md
AGENTS.md AGENTS.md
ROADMAP.md ROADMAP.md
AUDIT.md
``` ```
- Keep pure model/optimizer logic importable and testable without Home Assistant installed. - Keep pure model/optimizer logic importable and testable without Home Assistant installed.
+51
View File
@@ -269,3 +269,54 @@ poll with missing shared inputs now reports `waiting_for_inputs` instead of noth
**Verification.** `sh scripts/check.sh` passes in both environments: lint, 80 **Verification.** `sh scripts/check.sh` passes in both environments: lint, 80
offline tests, 20 HA Core tests, compilation, package checks. The offline tests offline tests, 20 HA Core tests, compilation, package checks. The offline tests
also pass on Python 3.9.6. also pass on Python 3.9.6.
## 0.4.0 — zones, electrical heat, ventilation, wind, inter-zone exchange
Scope: repository code and tests (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.
Design decisions and why:
- **Zones as config subentries.** HA's native pattern for several items under one
integration gives per-item add/edit/delete and removes a zone's entities with it.
Config entry version 3; migrating from 2 keeps each room's sensor ID as the zone
key so learned models, entities and statistics carry over. Rollback needs the
backup.
- **Layered model per zone.** The 0.3 base model is kept unchanged as fallback and
as the comparison every forecast is scored against; the extended and ventilation
layers learn only from intervals where their inputs are known, start at 0 with
priors sized in "typical samples", and report confidence. This follows the
roadmap rule to retain the simpler model and justify complexity by measured
improvement.
- **Ventilation separated from envelope loss.** Open, unknown or recovering windows
exclude a zone from base and extended learning; a separate learner fits the
residual. On the synthetic week, not doing this produced a −0.013 °C/5 min bias
and an 11 % inflated boiler coefficient.
- **Baseline-centred electrical heat.** The persistent load is already in the bias
term; only variation above a 24-hour 20th percentile is modeled, after a
first-order response driven by the power held over each interval.
- **Bounded directional wind term.** |vent_wind_dir| ≤ vent_wind, so leeward
windows still ventilate; angles use wrap-around differences.
- **Forecast assumptions** are single, conservative scenarios (electrical heat
fades with a half-life, open windows close after a set time) and are
configurable; no uncertainty band yet.
- **Exact compiled step** for the plan search: all terms are affine in temperatures
and lag, so coefficients are read off the real model (verified to 1e-12); the
180-minute search is 4.5× faster.
Evidence (synthetic apartments, 60-minute error, full vs base vs no change): gaming
office 0.031 / 0.182 / 0.40 °C; ventilation bedroom 0.026 / 0.218 / 0.52 °C; two
coupled rooms 0.024 / 0.037 / 0.29 °C. Coefficients recovered: electrical, boiler
and loss within 10 %; combined natural+wind ventilation within 15 %; recovery within
15 %; wind direction within 30 %. Coupling is found but correctly reported as not
identified when the rooms' difference is steady.
Not done, by decision: learned response time constants (configurable; compare
candidates out of sample next), a wall-mass state, forecast scenarios/uncertainty,
weather forecasts. Learners of removed zones keep only the base model.
**Verification.** `sh scripts/check.sh` passes in both environments (lint, 125
offline tests, 26 HA Core tests, compilation, package/translation/changelog checks).
The offline tests also pass on Python 3.9.6.
+73
View File
@@ -2,6 +2,79 @@
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).
## 0.4.0 — 2026-10-27
Thermal zones, electrical heat sources, windows with ventilation and wind, and
heat exchange between zones (Milestone 2, first part). Still **observation-only**:
no new service calls, the boiler and thermostat are untouched. Tested in the Home
Assistant Core harness with HA 2025.12.0 and 2026.2.3; not yet on HAOS.
### Upgrade notes
- **Back up Home Assistant first.** The config entry moves to version 3: each room
sensor becomes a one-sensor **zone** with its comfort weight, and the sensor's
entity ID stays the zone's key, so learned models, forecast entities and
accuracy statistics carry over unchanged. No model reset is needed.
- **Downgrading needs the backup.** Older versions refuse a version 3 entry.
- Configure no longer asks for room sensors or weights; manage zones with **Add
zone** and each zone's edit button on the integration page.
- Status attributes renamed: `input_quality.rooms` → `input_quality.zones` (each
zone lists its sensors), `excluded_rooms` → `excluded_zones`,
`retired_room_models` → `retired_zone_models`. The Forecast Error attribute
`rooms` is now `zones`, keyed by zone name. The `invalid_weights` status is gone
(invalid stored weights fall back to 1.0). Update templates that used these.
- New layers start with no assumed effect and need data: the electrical term needs
gaming sessions (or other load changes), the ventilation terms need window
openings. Watch `improvement_vs_base` on the Forecast Error sensors before
relying on them; switch off optional terms that do not help.
### Added
- **Zones** (config subentries): name, several sensors with sensor weights, comfort
weight, target offset and adjacent zones. Sensor and comfort weights never change
learned thermal coefficients.
- **Heat sources**: active power (W/kW; VA and % rejected), zone, heat fraction,
automatic 24-hour baseline or manual value, "included in" another source (no
double counting), thermal response time, enable/disable.
- **Windows**: contact sensor (optional inverted polarity; unknown stays unknown),
zone, facing direction (default 345°), opening factor, enable/disable; a bounded
log of ventilation events.
- **Wind direction** and **wind gust** weather sources with priorities and fallback;
knots and ft/s accepted.
- Model options: electrical-heat forecast half-life, assumed open-window time,
recovery time constant, and wind / gust / cross-ventilation terms.
- Per-zone diagnostics on each zone's forecast entity: current temperature,
+15/+30/+60/+120-minute forecast, contributions in °C/h, coefficients with
confidence; Status lists heat sources, windows, ventilation and recent events.
- Forecast Error sensors: `base_model_mae` and `improvement_vs_base`.
- Poll outcomes `sensors_changed` and `ventilation`, with English and Romanian names.
### Changed
- Each zone's model is layered: the unchanged base model (fallback and
comparison), an extended layer with electrical heat and neighbours, and a
separate ventilation layer. Zones with an open, unknown or recovering window are
kept out of base and extended learning.
- Forecasts simulate all zones together, with electrical heat fading to baseline
and open windows closing after the configured time; the plan search uses an exact
compiled step and stays fast at 180 minutes.
- Accuracy scoring replays all zones with the actual relay, electrical heat and
window history, and scores the base model on the same spans.
### Persisted data
- Model payload `schema_version` stays 2 with optional new keys: `zone_models`,
`ventilation_models`, `heat_sources` (baseline samples) and `ventilation`
(events). Forecast statistics gain a base-model column; 0.3 statistics load.
### Development
- New pure modules: `learning.py` (named-feature RLS), `zones.py`, `heat_sources.py`,
`windows.py`, `zone_model.py`; [docs/MODEL.md](docs/MODEL.md) documents the model.
- Synthetic-apartment tests for gaming heat, ventilation with wind and recovery,
and coupled rooms, each checking recovered coefficients and out-of-sample
improvement over the base model.
## 0.3.0 — 2026-10-26 ## 0.3.0 — 2026-10-26
First part of Milestone 1: the integration now measures how accurate its thermal First part of Milestone 1: the integration now measures how accurate its thermal
+114 -48
View File
@@ -1,4 +1,4 @@
# Heating MPC 0.3.0 — custom integration for Home Assistant OS # Heating MPC 0.4.0 — custom integration for Home Assistant OS
**Experimental, observation-only (shadow mode).** It **never** changes the Generic Thermostat, the physical relay, the boiler's setpoint, `heat/off`, or Comfort/Away/Sleep. No Docker add-on, MQTT or InfluxDB connection is needed. **Experimental, observation-only (shadow mode).** It **never** changes the Generic Thermostat, the physical relay, the boiler's setpoint, `heat/off`, or Comfort/Away/Sleep. No Docker add-on, MQTT or InfluxDB connection is needed.
@@ -10,20 +10,60 @@ This project targets Home Assistant OS on a Raspberry Pi 5, with one existing ON
2. Copy the entire folder `custom_components/heating_mpc` into `/config/custom_components/heating_mpc` (replace its contents when upgrading; read the upgrade notes in [CHANGELOG.md](CHANGELOG.md) first). 2. Copy the entire folder `custom_components/heating_mpc` into `/config/custom_components/heating_mpc` (replace its contents when upgrading; read the upgrade notes in [CHANGELOG.md](CHANGELOG.md) first).
3. Restart Home Assistant **Core**. 3. Restart Home Assistant **Core**.
4. If new: **Settings → Devices & services → Add integration → Heating MPC**. If installed already: select **Heating MPC → Configure** (Options). 4. If new: **Settings → Devices & services → Add integration → Heating MPC**. If installed already: select **Heating MPC → Configure** (Options).
5. Choose the existing thermostat and physical heating switch (read-only), then individual room sensor entities. 5. Choose the existing thermostat and physical heating switch (read-only), then room temperature sensors. Each sensor becomes a zone; group them later (see below).
6. Choose *ordered lists* of **outdoor sensor entity IDs**. First valid sensor is active; if missing, unavailable, nonnumeric or outside reasonable bounds, the next valid sensor is used automatically. 6. Choose *ordered lists* of **outdoor sensor entity IDs**. First valid sensor is active; if missing, unavailable, nonnumeric or outside reasonable bounds, the next valid sensor is used automatically.
7. Assign a relative comfort weight (0.1–10) to each selected room. 7. Assign a relative comfort weight (0.1–10) to each selected sensor's zone.
8. Optionally add zones, heat sources and windows from the integration page (next section).
On Home Assistant 2026.3 or newer the integration shows its own icon (a radiator with a forecast curve) in **Devices & services**; older versions show a generic placeholder. On Home Assistant 2026.3 or newer the integration shows its own icon (a radiator with a forecast curve) in **Devices & services**; older versions show a generic placeholder.
**No entity IDs are hard-coded.** Every room and outdoor measurement can be changed later via **Configure**, without editing YAML or Python. **No entity IDs are hard-coded.** Every sensor, zone, heat source, window and outdoor measurement can be changed later in the UI, without editing YAML or Python.
## Zones, heat sources and windows
On **Settings → Devices & services → Heating MPC**, the buttons **Add zone**, **Add
heat source** and **Add window** create items that can each be edited or deleted
later. **Configure** holds everything else: thermostat, relay, weather sources,
report-age limits, MPC settings and model options.
**Zones** are rooms or areas. Each has a name, one or more temperature sensors with
sensor weights (how much each counts in the zone temperature), a comfort weight
(how much the zone matters to the MPC), an optional target offset relative to the
thermostat's target, and optional adjacent zones it exchanges heat with. Sensor and
comfort weights never change the learned thermal coefficients. A sensor belongs to
one zone only. Upgrading from 0.3 turns each room sensor into a one-sensor zone
with the same comfort weight; to group two sensors of one room, edit one zone to
include both and delete the other.
**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
rejected), the zone, the share of the power that becomes heat there (1.0 by
default), the baseline (automatic: the low end of the last 24 hours; or a manual
value in W), whether its reading is **already included in** another source (for a
PC measured separately behind a UPS, so it is not counted twice), and the thermal
response time (default 15 minutes). Only the variation above the baseline is
modeled; the persistent load is already part of the zone's learned behavior.
**Windows** need a contact sensor (binary sensor), the zone, whether the sensor
reports *on* when closed (inverted), the direction the outside of the window faces
(degrees clockwise from north; default 345°, editable) and an opening factor
(1.0 normal, lower for tilted, higher for large). Unknown or unavailable states are
treated as unknown, never as closed.
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
stay open (default 15 minutes), the recovery time constant after closing (default
20 minutes), and whether to use wind, wind gusts and cross-ventilation terms.
See [docs/MODEL.md](docs/MODEL.md) for the equations and what is learned when.
## Reconfigurable weather inputs ## Reconfigurable weather inputs
| Measurement | Required? | Used in the thermal model? | | Measurement | Required? | Used in the thermal model? |
|---|---|---| |---|---|---|
| Outdoor temperature | **Yes** (1–10 prioritized sensors) | **Yes**: estimates thermal losses; constant over the 2 h forecast window | | Outdoor temperature | **Yes** (1–10 prioritized sensors) | **Yes**: estimates thermal losses; constant over the 2 h forecast window |
| Wind speed | No | No, diagnostics/source comparison only | | Wind speed | No | Yes, for open windows (ventilation), when enabled |
| Wind direction (degrees the wind comes from) | No | Yes, with the windows' facing direction |
| Wind gust | No | Only if the gust option is enabled |
| Cloud cover | No | No, diagnostics/source comparison only | | Cloud cover | No | No, diagnostics/source comparison only |
| Humidity | No | No, diagnostics/source comparison only | | Humidity | No | No, diagnostics/source comparison only |
| Feels-like temperature | No | No, diagnostics/source comparison only | | Feels-like temperature | No | No, diagnostics/source comparison only |
@@ -33,29 +73,35 @@ On Home Assistant 2026.3 or newer the integration shows its own icon (a radiator
Choose multiple sensors for a single variable, e.g. OpenWeatherMap then another provider, in priority order. Switch order in **Configure** to run A/B comparisons on successive days, or simply let the second sensor act as fallback. The active source and all current alternative readings are visible in the **Heating MPC Status** entity attributes (`weather`, `weather_active_sources`, `weather_comparison`). The **MPC Outdoor Temperature** sensor also has an `active_source` attribute and an `alternatives` map. Choose multiple sensors for a single variable, e.g. OpenWeatherMap then another provider, in priority order. Switch order in **Configure** to run A/B comparisons on successive days, or simply let the second sensor act as fallback. The active source and all current alternative readings are visible in the **Heating MPC Status** entity attributes (`weather`, `weather_active_sources`, `weather_comparison`). The **MPC Outdoor Temperature** sensor also has an `active_source` attribute and an `alternatives` map.
Measurements are normalized where possible: outdoor temperatures to °C; wind to m/s; pressure to hPa; visibility to meters. Unknown units or values outside plausibility bounds are rejected in favor of the next selected sensor. Clouds and humidity are expected in %, rain/snow in mm or mm/h as represented by their upstream sensor. A fallback source may have a different sampling delay; configurable report-age limits now reject stale inputs (see below). Measurements are normalized where possible: outdoor temperatures to °C; wind and gusts to m/s (from km/h, mph, kn, ft/s); wind direction to degrees (360 = 0); pressure to hPa; visibility to meters. Unknown units or values outside plausibility bounds are rejected in favor of the next selected sensor. Clouds and humidity are expected in %, rain/snow in mm or mm/h as represented by their upstream sensor. A fallback source may have a different sampling delay; configurable report-age limits now reject stale inputs (see below).
Switching only the **weather-provider sensors** does not reset learned thermal parameters. The update is applied after the integration reloads, and it skips learning one transition when the primary temperature source changes, avoiding a jump in the training series. Nevertheless, two sensors with a persistent offset can bias predictions; compare their readings and validate the model again before relying on them. Replacing the physical heating switch discards incompatible model parameters. Existing room-specific parameters are retained if room entity IDs remain unchanged. Switching only the **weather-provider sensors** does not reset learned thermal parameters. The update is applied after the integration reloads, and it skips learning one transition when the primary temperature source changes, avoiding a jump in the training series. Nevertheless, two sensors with a persistent offset can bias predictions; compare their readings and validate the model again before relying on them. Replacing the physical heating switch discards incompatible model parameters. Learned zone parameters are kept as long as the zone exists (its key does not change when you edit it).
## Model, sampling and output ## Model, sampling and output
- Every 5 minutes: reads HA sensor states and observed relay state; learns a per-room linear thermal model `dT = a*(T_out-T_room) + b*effective_heating + c`. - Every 5 minutes: reads sensor, relay, power and window states and learns a per-zone model in layers (full description in [docs/MODEL.md](docs/MODEL.md)):
- Learns only from valid 5-minute transitions; uses a smoothed heating-demand proxy to approximate radiator lag. Relay changes are timestamped by a read-only state listener, so the lag estimate follows every switch between polls. - **base**: `dT = loss*(T_out-T) + heat*lag + bias`, always learned, the fallback and comparison;
- After at least 200 transitions and samples with both heating states, runs an ON/OFF MPC search for the next 60–180 minutes (default 120) in 15-minute command blocks. Protects against rapid simulated toggling. - **extended**: adds electrical heat above baseline and exchange with adjacent zones;
- Uses room comfort weights and the **existing** Generic Thermostat's target and heat/off state. **The simulated ON/OFF demand never operates hardware**. - **ventilation**: learned separately while a window is open or the room recovers, so open windows never look like poor insulation.
- Persists model parameters using HA's `.storage` so HA restarts do not discard the trained data. Models of rooms removed in Configure are kept (up to 24) and restored if the room is added back. - New effects start at zero and report a confidence; nothing is assumed until the data shows it. Relay changes are timestamped by a read-only state listener.
- After 200 samples per zone, including both relay states, runs an ON/OFF MPC search for the next 60–180 minutes (default 120) in 15-minute command blocks, simulating all zones together with assumed electrical heat and window behavior.
- Uses zone comfort weights and target offsets with the **existing** Generic Thermostat's target and heat/off state. **The simulated ON/OFF demand never operates hardware**.
- Persists everything learned in HA's `.storage`. Models of zones removed in the UI are kept (up to 24) and restored if the zone is added back.
Entities (entity IDs may vary): Entities (entity IDs may vary):
- `sensor.heating_mpc_status` — learning/ready/paused status and diagnostics, including outside weather, active source, all valid alternative weather values, model coefficients, proposed 15-minute plan. - `sensor.heating_mpc_status` — learning/ready/paused status and diagnostics, including outside weather, active source, all valid alternative weather values, model coefficients, proposed 15-minute plan.
- `sensor.heating_mpc_weighted_temperature` — own room-weighted average, informative only. - `sensor.heating_mpc_weighted_temperature` — comfort-weighted average of the zones, informative only.
- `sensor.mpc_outdoor_temperature` — selected external temp and alternatives. - `sensor.mpc_outdoor_temperature` — selected external temp and alternatives.
- Per-room predicted temperatures after 60 minutes. Attributes `forecast_15min` (the - One forecast entity per zone (**MPC <zone> 60min Prediction**), state = the zone's
temperature every 15 minutes over the horizon) and `plan_15min` (the proposed temperature in 60 minutes. Attributes: `current_temperature`, `forecast` (+15/+30/
ON/OFF plan). These assume the MPC's plan is executed; while Generic Thermostat +60/+120 minutes as far as the horizon goes), `forecast_15min`, `plan_15min`,
controls the relay they are **counterfactual**. `model_in_use`, `contributions_c_per_h` (what drives the zone right now: loss,
heat, bias, internal, neighbors, ventilation, recovery) and `coefficients` with
confidence. Plan forecasts assume the MPC's plan is executed; while Generic
Thermostat controls the relay they are **counterfactual**.
- `binary_sensor.heating_mpc_proposed_heat` — **hypothetical** heating demand (unavailable until ready). - `binary_sensor.heating_mpc_proposed_heat` — **hypothetical** heating demand (unavailable until ready).
- **Heating MPC Learning Progress** — how close the least trained room is to being - **Heating MPC Learning Progress** — how close the least trained zone is to being
used for forecasts (100 % = ready); attributes show the sample counts and what is used for forecasts (100 % = ready); attributes show the sample counts and what is
required. required.
- **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
@@ -67,7 +113,8 @@ Entities (entity IDs may vary):
## 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.
- The other meteorological variables besides temperature are intentionally **captured but not yet fitted into the thermal model**. This allows source tests without falsely treating weak inputs as validated improvements. Later versions can test whether wind or solar proxy reduces held-out forecast error. - 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`).
- 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.
- No Recorder/InfluxDB training backfill. It learns only from samples collected after installation. - No Recorder/InfluxDB training backfill. It learns only from samples collected after installation.
@@ -89,46 +136,61 @@ After a Home Assistant restart that time is unknown, so learning waits until the
estimate is within 10 % (*Waiting for radiators to settle*), about 32 minutes. estimate is within 10 % (*Waiting for radiators to settle*), about 32 minutes.
Forecasts continue meanwhile. Reloading the integration usually does not cause this Forecasts continue meanwhile. Reloading the integration usually does not cause this
wait, because the relay's last change is still known. *Ready* then needs 200 wait, because the relay's last change is still known. *Ready* then needs 200
accepted samples (about 17 hours) per room, including 30 with the relay ON and 30 accepted samples (about 17 hours) per zone, including 30 with the relay ON and 30
with it OFF, which can take a few days depending on how often the boiler runs. 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`,
`learning`, `paused` (thermostat not in `heat`), `waiting_for_target`, `no_zones` (add a zone), `learning`, `paused` (thermostat not in `heat`), `waiting_for_target`, `ready`,
`invalid_weights`, `ready`, `inputs_changed` or `prediction_error`. Proposed heat `inputs_changed` or `prediction_error`. Proposed heat is available only while it
is available only while it is `ready`. Useful attributes: is `ready`. Useful attributes:
- `input_quality` — for each room and each weather source (grouped by metric): - `input_quality` — per zone (with each of its sensors) and per weather source
`status` (`valid`, `missing`, `unknown`, `unavailable`, `stale`, (grouped by metric): `status` (`valid`, `missing`, `unknown`, `unavailable`,
`invalid_timestamp`, `future_timestamp` or `invalid_value_or_unit`), `stale`, `invalid_timestamp`, `future_timestamp` or `invalid_value_or_unit`),
`age_seconds` (time since the sensor last reported to HA) and `age_seconds` (time since the sensor last reported to HA) and
`max_age_seconds_24h` (largest age seen at polls in the last 24 hours; resets on `max_age_seconds_24h` (largest age seen at polls in the last 24 hours; resets on
reload and can read up to one poll, 5 minutes, low). Rooms also have `used`: reload and can read up to one poll, 5 minutes, low). Zones and sensors have `used`.
whether that room's reading was used in this poll. - `excluded_zones` and `weight_coverage` — zones left out of this poll, and the
- `excluded_rooms` and `weight_coverage` — rooms left out of this poll, and the share of the total comfort weight still covered by the zones used (1.0 = all).
share of the total comfort weight still covered by the rooms used (1.0 = all). `zone_names` maps zone keys to names.
- `heat_sources` — per source: measured and effective power (after removing
included sources), baseline, variable power above it, thermal response, whether it
has settled.
- `windows` — per window: state, minutes open, wind direction relative to the
window's facing and the resulting exposure (1 = wind straight at it).
`ventilation` — per zone: current opening, recovery, temperature before opening,
its contribution in °C/h and the ventilation coefficients. `ventilation_events` —
the last five openings with temperatures before, lowest, at closing and 30
minutes after, and the weather.
- `heat_lag` and `heat_lag_uncertainty` — the radiator-lag estimate (0 cold, 1 fully - `heat_lag` and `heat_lag_uncertainty` — the radiator-lag estimate (0 cold, 1 fully
warm) and its error bound. Learning waits while the bound is above 0.1, for warm) and its error bound. Learning waits while the bound is above 0.1, for
example for about 30 minutes after an HA restart. example for about 30 minutes after an HA restart.
- `sample_result` — why the last poll was or was not learned: `first_sample`, - `sample_result` — why the last poll was or was not learned: `first_sample`,
`learned`, `partly_rejected` (an outlier in some room), `interval_out_of_range`, `sensors_changed` (a zone's set of usable sensors changed), `learned`,
`relay_changed`, `outside_source_changed` or `lag_uncertain`. `ventilation` (only window-affected zones; given to the ventilation model),
- `models` — per-room coefficients, accepted sample counts and the in-sample `partly_rejected` (an outlier in some zone), `interval_out_of_range`,
5-minute error (`mae_5m`), which is **not** a forecast accuracy. `relay_changed`, `outside_source_changed`, `lag_uncertain` or `waiting_for_inputs`.
- `retired_room_models` — removed rooms whose learned models are kept. - `models` — per-zone base coefficients, accepted sample counts and the in-sample
5-minute error (`mae_5m`, **not** a forecast accuracy), `model_in_use` and the
extended layer's coefficients with confidence.
- `retired_zone_models` — removed zones whose learned models are kept.
`input_quality` and `weather_comparison` change every poll, so they are not `input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation`
written to the Recorder; the other attributes are. and `ventilation_events` change every poll, so they are not written to the
Recorder; the other attributes are.
### Choosing freshness limits ### Choosing freshness limits
Room and weather report-age limits are set during setup and in **Configure** Room and weather report-age limits are set during setup and in **Configure**
(5–1440 minutes; defaults 30 for rooms and 120 for weather). (5–1440 minutes; defaults 30 for rooms and 120 for weather).
A room sensor that is older than its limit, unavailable, missing or invalid is A zone sensor that is older than its limit, unavailable, missing or invalid is
**left out**: the weighted temperature, learning and forecasts continue with the **left out**; the zone uses its other sensors (without learning across that change).
other rooms, whose weights are re-normalized. Status shows it in `excluded_rooms`. A zone with no usable sensor is left out entirely: the weighted temperature,
Learning and forecasts pause only when no room is usable, or when the relay, learning and forecasts continue with the
thermostat or outdoor temperature is missing. A room that reports again learns other zones, whose weights are re-normalized. Status shows it in `excluded_zones`.
Learning and forecasts pause only when no zone is usable, or when the relay,
thermostat or outdoor temperature is missing. A zone that reports again learns
from its second poll onwards, so nothing is learned across its gap. from its second poll onwards, so nothing is learned across its gap.
The checkbox **Keep using the last value of a room sensor that stopped reporting** The checkbox **Keep using the last value of a room sensor that stopped reporting**
@@ -155,16 +217,17 @@ age limit.
## Forecast accuracy ## Forecast accuracy
The four **Forecast Error** sensors measure the thermal model against reality, The four **Forecast Error** sensors measure the thermal model against reality,
continuously and out of sample. At every poll the integration records each room's 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 reading, the outdoor temperature, the radiator-lag estimate and the model's
parameters. 15, 30, 60 and 120 minutes later it replays that room from the earlier 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 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 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. 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 This tests the thermal model, not the MPC plan, because the plan is never
executed. Each sensor's state is the mean absolute error (°C) over the last 7 days, executed. All zones are replayed together, with the electrical heat and window
across all rooms. Attributes, all in °C except counts: openings that actually happened, and the base model is scored on the same spans. Each sensor's state is the mean absolute error (°C) over the last 7 days,
across all zones. Attributes, all in °C except counts:
- `samples` — number of scored forecasts. Consecutive forecasts overlap, so they - `samples` — number of scored forecasts. Consecutive forecasts overlap, so they
are not independent; treat small counts as indicative only. are not independent; treat small counts as indicative only.
@@ -172,11 +235,14 @@ across all rooms. Attributes, all in °C except counts:
- `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.
- `rooms` — the same numbers per room (not written to the Recorder). - `base_model_mae`, `improvement_vs_base` — the base model's error on the same
spans, and the share of it the extra layers remove (0 = no gain, negative = they
make it worse: switch that option off).
- `zones` — the same numbers per zone (not written to the Recorder).
Forecasts are scored only where the inputs were known: polls 4.5–5.5 minutes Forecasts are scored only where the inputs were known: polls 4.5–5.5 minutes
apart, a usable reading at the start and the end, and a well-known radiator-lag apart, a usable reading at the start and the end, and a well-known radiator-lag
estimate throughout. A missing relay, outdoor temperature or all rooms restarts the estimate throughout. A missing relay, outdoor temperature or all zones restarts the
replay window, so scores resume 15–120 minutes later. Statistics are kept with the replay window, so scores resume 15–120 minutes later. Statistics are kept with the
model and survive restarts; deleting the model resets them. model and survive restarts; deleting the model resets them.
+16 -11
View File
@@ -10,7 +10,7 @@ Success means predictable comfort, understandable decisions, safe operation and
## Current baseline — v0.2.1 ## Current baseline — v0.2.1
> **Current release: 0.3.0** — Milestone 0 reliability work and the first part of Milestone 1 on top of this > **Current release: 0.4.0** — Milestone 0 reliability work, part of Milestone 1 and the first part of Milestone 2 on top of this
> baseline; see [CHANGELOG.md](CHANGELOG.md). The lists below describe 0.2.1. > baseline; see [CHANGELOG.md](CHANGELOG.md). The lists below describe 0.2.1.
**Implemented (experimental):** **Implemented (experimental):**
@@ -104,14 +104,14 @@ recording volatile attributes, and tests against HA 2025.12.0 and 2026.2.3.
**Objective:** Improve forecasts with genuinely useful features, not just more inputs. **Objective:** Improve forecasts with genuinely useful features, not just more inputs.
- [ ] Compare existing per-room linear model with a multi-zone RC model including inter-room heat exchange. - [x] Compare existing per-room linear model with a multi-zone RC model including inter-room heat exchange. *(0.4.0: layered zone model with neighbour exchange; base model scored alongside on every forecast. Exchange between zones sharing one boiler is weakly identifiable and reported as such.)*
- [ ] Address multiple sensors within a single physical room; configurable room grouping, aggregation and comfort priorities. - [x] Address multiple sensors within a single physical room; configurable room grouping, aggregation and comfort priorities. *(0.4.0: zones with sensor weights, comfort weights and target offsets.)*
- [ ] Distinguish air/room thermal mass and radiator lag where observable; avoid overfitting unidentifiable parameters. - [ ] Distinguish air/room thermal mass and radiator lag where observable; avoid overfitting unidentifiable parameters.
- [ ] Add optional Recorder history import/backfill for **indoor sensor + physical relay** history; do not assume old HA Recorder data still exists. - [ ] Add optional Recorder history import/backfill for **indoor sensor + physical relay** history; do not assume old HA Recorder data still exists.
- [ ] Add outdoor forecast data with future timestamps (forecast provider selectable) and compare forecast-based versus constant-outdoor assumptions. - [ ] Add outdoor forecast data with future timestamps (forecast provider selectable) and compare forecast-based versus constant-outdoor assumptions.
- [ ] Experiment with wind/cloud cover as optional covariates; retain them only if held-out forecast error improves. Solar gains may require orientation/irradiance or a useful proxy. - [ ] Experiment with wind/cloud cover as optional covariates; retain them only if held-out forecast error improves. Solar gains may require orientation/irradiance or a useful proxy. *(0.4.0: wind speed, direction and gusts for open windows, optional and judged by `improvement_vs_base`; clouds and solar still open.)*
- [ ] Expose source reliability and provider comparison: normalized units, update age, outages and sensor offsets. - [ ] Expose source reliability and provider comparison: normalized units, update age, outages and sensor offsets.
- [ ] Handle shifts in room usage, opened windows, sensor relocation and season changes without unstable feedback. - [ ] Handle shifts in room usage, opened windows, sensor relocation and season changes without unstable feedback. *(0.4.0: opened windows handled by a separate ventilation layer; electrical loads by heat sources. Sensor relocation and seasons still open.)*
- [ ] Benchmark RAM, CPU and event-loop latency on Raspberry Pi 5. Prefer standard-library/small scientific dependencies and avoid heavy inference runtimes. - [ ] Benchmark RAM, CPU and event-loop latency on Raspberry Pi 5. Prefer standard-library/small scientific dependencies and avoid heavy inference runtimes.
**Exit criteria:** Model complexity justified by reproducible forecast improvements over v0.3.0; stable fitting and manageable HAOS resource usage. Retain the simpler model as fallback where it performs better. **Exit criteria:** Model complexity justified by reproducible forecast improvements over v0.3.0; stable fitting and manageable HAOS resource usage. Retain the simpler model as fallback where it performs better.
@@ -174,12 +174,17 @@ Items 1–3 of the original backlog are done (see AUDIT.md and CHANGELOG.md 0.2.
room out, or optionally keep its last value for up to 24 hours). Before live room out, or optionally keep its last value for up to 24 hours). Before live
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. Milestone 1 (rest, 0.3.x): let the Forecast Error sensors collect a few weeks 3. Configure zones, the UPS heat source and the windows on HAOS; after a week of
of heating data and review per-room bias; a dashboard example; forecast heating review `improvement_vs_base`, per-zone bias and which coefficients are
timestamps/provenance; a fixed-parameter baseline; explanations for the proposed identified. Switch off optional terms that do not help.
action; an actual-policy forecast that simulates Generic Thermostat. 4. Milestone 1 (rest): a dashboard example; forecast timestamps/provenance;
4. Only then consider richer weather inputs and the live-control **design**; do not explanations for the proposed action; an actual-policy forecast that simulates
implement live control without separate authorization. Generic Thermostat.
5. Milestone 2 (rest): learned response times (compare candidate time constants
out of sample), a wall-mass state if recovery data shows it is needed, Recorder
backfill, weather forecasts, and a Raspberry Pi 5 resource benchmark.
6. Only then consider the live-control **design**; do not implement live control
without separate authorization.
## Definition of done for each milestone ## Definition of done for each milestone
+1 -1
View File
@@ -9,5 +9,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": [], "requirements": [],
"single_config_entry": true, "single_config_entry": true,
"version": "0.3.0" "version": "0.4.0"
} }
+218
View File
@@ -0,0 +1,218 @@
# Heating MPC thermal model
This document describes the model used from version 0.4.0: what it computes, the
units and meaning of every coefficient, when each part learns, what the forecasts
assume, and how the model is validated. It is observational: nothing here controls
the boiler.
## Time step and inputs
The model works in 5-minute steps, the polling interval. For each zone *i*, each
poll provides:
| Symbol | Meaning | Unit | Source |
|---|---|---|---|
| T_i | Zone temperature: sensor-weighted mean of its usable sensors | °C | zone sensors |
| T_out | Outdoor temperature (first fresh, valid source) | °C | weather sources |
| lag | Radiator-lag proxy, 0 (cold) to 1 (fully warm); 20-minute first-order response to the relay, integrated at the relay's actual switch times | – | relay state + read-only listener |
| q_i | Electrical heat above baseline after its thermal response | kW | heat sources |
| O_i | Sum of open windows' opening factors, weighted by the share of the interval they were open | – | window sensors |
| F_i | Same, each window also weighted by cos(angle between wind direction and the window's facing) | – | windows + wind direction |
| v, g | Wind speed and gust speed | m/s | weather sources |
| R_i | Recovery weight after the zone's last window closed: exp(−t / τ_rec), 0 below 0.05 | – | window sensors |
| T_ref,i | Zone temperature at the poll before the window opened | °C | zone sensors |
## Equations
Per 5-minute step, the predicted change of zone *i* is the sum of layers:
```text
ΔT_i = loss × (T_out − T_i) envelope heat loss
+ heat × lag boiler, through the radiators
+ bias constant gains and losses
+ internal × q_i electrical heat above baseline (extended layer)
+ Σ_j couple:j × (T_j − T_i) exchange with adjacent zones (extended layer)
+ vent_natural × O_i (T_out − T_i) (ventilation layer)
+ vent_wind × O_i v (T_out − T_i)
+ vent_wind_dir × F_i v (T_out − T_i)
+ vent_gust × O_i max(0, g − v) (T_out − T_i) (optional)
+ vent_cross × O_i O_other (T_out − T_i) (optional)
+ recovery × R_i (T_ref,i − T_i)
```
All coefficients are in °C per 5 minutes per unit of their term. They are
statistical estimates, not measured physical properties: `heat` is not boiler
efficiency, and a relay that is on does not prove the burner is firing.
In conceptual continuous-time form this corresponds to
```text
C_i dT_i/dt = UA_i (T_out − T_i) + Q_boiler + Q_internal + Q_ventilation + Σ_j K_ij (T_j − T_i) + Q_other
```
divided through by the zone's effective heat capacity C_i and discretized to 5
minutes. The model is first-order (one temperature per zone). Thermal inertia
enters through the radiator lag, the electrical response filter and the recovery
term; a separate wall-mass state is not modeled (see Limitations).
### Base layer
The base layer is the 3-parameter model of earlier versions (loss, heat, bias),
learned per zone. It is always kept: it is the fallback whenever another layer's
inputs are unknown, and the comparison that shows whether the other layers help.
### Extended layer
Present when a zone has an enabled heat source or an adjacent zone. It learns all
base terms plus `internal` and `couple:<zone>` and replaces the base layer in
forecasts once it is as trained as the base layer needs to be (200 samples, 30 with
the relay on, 30 off). If any of its inputs is unknown, the base layer is used.
### Electrical heat
For each enabled source:
```text
effective power = measured W − measured W of enabled sources included in it (≥ 0)
baseline = 20th percentile of the last 24 h of effective power, or the manual value
variable heat = heat fraction × (effective power − baseline) / 1000 (kW)
thermal response : r ← r + (1 − exp(−5 min / τ)) × (variable heat held during the interval − r)
```
q_i is the sum of the responses of the zone's sources. Centring on the baseline
matters: the persistent load (for example a UPS drawing 200 W all day) is already
part of the bias term, so feeding it again would count it twice. The baseline
needs one hour of data; after startup or an outage a source's response must run
for two time constants before it is used for learning.
Only active power counts (W, kW, mW). VA (apparent power) and % (UPS load) are
rejected. A UPS output sensor does not include the UPS's own losses; if they matter,
measure the input side or lower the heat fraction accordingly.
### Ventilation layer
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
open, its state is unknown, or the zone is recovering. During those periods the
base and extended layers do **not** learn, so window events cannot inflate `loss`
or shift `bias`.
`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.
Wind direction is meteorological: the direction the wind comes **from**, degrees
clockwise from north. A window's azimuth is the direction its outside faces. Wind
from the azimuth blows straight at the window (cos = 1); wind from behind the
building gives −1. Angle differences wrap around north correctly.
## Priors, bounds and confidence
| Coefficient | Prior | Bounds | Prior worth |
|---|---|---|---|
| loss | 0.002 | 0 … 0.04 | as in earlier versions |
| heat | 0.045 | 0 … 0.25 | as in earlier versions |
| bias | 0 | −0.06 … 0.06 | as in earlier versions |
| internal | 0 | 0 … 0.5 | 20 samples at 0.4 kW |
| couple:j | 0 | 0 … 0.05 | 200 samples at 1 °C difference |
| vent_natural | 0 | 0 … 0.2 | 20 samples at opening 1, 15 °C difference |
| vent_wind, vent_wind_dir | 0 | 0 … 0.03 / ±vent_wind | 20 samples at 4 m/s |
| vent_gust | 0 | 0 … 0.03 | 20 samples |
| vent_cross | 0 | 0 … 0.1 | 100 samples |
| recovery | 0 | 0 … 0.5 | 20 samples at 0.5 °C deficit |
New effects start at 0: no effect is assumed until the data shows one. Learning is
recursive least squares with exponential forgetting (about 3.5 days of samples),
a covariance bound that stops windup during long periods without excitation, and
outlier rejection above 0.8 °C per 5 minutes.
Each coefficient reports:
* `confidence`: the share of its information that comes from data rather than the
prior (0 = prior only);
* `std`: its standard error, using a running estimate of the unexplained noise;
* `samples`: how many accepted samples actually exercised it;
* `identified`: at least 30 exercising samples and confidence ≥ 0.8.
A coefficient can move without being identified. Example from the tests: two rooms
whose temperature difference stays fairly steady cannot separate exchange between
them from each room's bias; the learner finds a positive coupling but correctly
reports it as not identified.
## Learning conditions
A zone learns from the interval between two polls only when:
* the polls are 4.5–5.5 minutes apart, the relay held one state throughout and the
radiator-lag estimate is known within 0.1 (for example ~32 minutes after an HA
restart);
* the outdoor source did not change and the zone used the same sensors at both
polls;
* for the extended layer, every source in the zone is settled and every adjacent
zone was usable;
* for the ventilation layer, every window state in the zone is known.
No experiments are run and the boiler is never switched to gather data: everything
is learned passively.
## Forecasts
Forecasts simulate all zones together, step by step, over the MPC horizon (60–180
minutes):
* **Weather:** held at current values.
* **Electrical heat:** the variable part fades towards baseline with a configurable
half-life (default 30 minutes; 0 = stops now), and the room keeps following it with
its response time. A gaming PC drawing 600 W now is not assumed to run on for two
hours.
* **Windows:** an open window is assumed to stay open for a configurable time
(default 15 minutes), then close; the zone then recovers.
* **Relay:** the candidate plan being evaluated.
For the plan search the step is compiled into exact per-zone coefficients (every
term is linear in the temperatures and the lag), which keeps the 180-minute search
fast on a Raspberry Pi.
Plan forecasts are **counterfactual** while Generic Thermostat controls the relay:
they describe what would happen under the MPC's proposed plan.
## Validation
The Forecast Error sensors score forecasts that are valid for comparison with
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;
* `persistence_mae`, `skill`: against assuming no change;
* `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).
Results on synthetic apartments (the tests; these check the method, not real-world
accuracy):
| Scenario | 60-min error, full | base | no change |
|---|---|---|---|
| Office, UPS 200 W + 400 W gaming sessions, thermostat cycling | 0.031 °C | 0.182 °C | 0.40 °C |
| Bedroom, 15–20 min ventilation every 4 h, wind and recovery | 0.026 °C | 0.218 °C | 0.52 °C |
| Two adjacent rooms, true exchange 0.01 | 0.024 °C | 0.037 °C | 0.29 °C |
In the ventilation scenario, the same data without the window configured produced
a −0.013 °C/5 min cooling bias and an 11 % inflated boiler coefficient, which is
the contamination the separate ventilation layer prevents.
## Limitations
* One temperature per zone: walls and air are not separate states. The recovery
term approximates the air warming back after ventilation.
* Response times (electrical heat, recovery) are configured, not learned. Compare
settings with `improvement_vs_base`.
* Coupling between zones heated by one boiler is weakly identifiable; it is shrunk
towards 0 and usually reported as not identified.
* Natural and wind ventilation terms both scale with the opening; with little wind
variation their split is uncertain even when their combined effect is accurate.
* Forecasts hold weather constant and use one scenario for electrical heat and
windows; no forecast uncertainty band is computed yet.
* Window sensors with a permanently unknown state keep their zone out of learning;
disable such a window.