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

+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.
@@ -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).
3. Restart Home Assistant **Core**.
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.
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.
**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
| 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 |
| 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 |
| Humidity | 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.
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
- 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`.
- 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.
- 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.
- Uses room comfort weights and the **existing** Generic Thermostat's target and heat/off state. **The simulated ON/OFF demand never operates hardware**.
- 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.
- 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)):
- **base**: `dT = loss*(T_out-T) + heat*lag + bias`, always learned, the fallback and comparison;
- **extended**: adds electrical heat above baseline and exchange with adjacent zones;
- **ventilation**: learned separately while a window is open or the room recovers, so open windows never look like poor insulation.
- 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):
- `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.
- Per-room predicted temperatures after 60 minutes. Attributes `forecast_15min` (the
temperature every 15 minutes over the horizon) and `plan_15min` (the proposed
ON/OFF plan). These assume the MPC's plan is executed; while Generic Thermostat
controls the relay they are **counterfactual**.
- One forecast entity per zone (**MPC <zone> 60min Prediction**), state = the zone's
temperature in 60 minutes. Attributes: `current_temperature`, `forecast` (+15/+30/
+60/+120 minutes as far as the horizon goes), `forecast_15min`, `plan_15min`,
`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).
- **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
required.
- **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
- **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.
- No weather forecast: assumes outdoor temperature remains constant during simulated horizon.
- 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.
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
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.
The **Heating MPC Status** entity's state is one of `waiting_for_sensors`,
`learning`, `paused` (thermostat not in `heat`), `waiting_for_target`,
`invalid_weights`, `ready`, `inputs_changed` or `prediction_error`. Proposed heat
is available only while it is `ready`. Useful attributes:
`no_zones` (add a zone), `learning`, `paused` (thermostat not in `heat`), `waiting_for_target`, `ready`,
`inputs_changed` or `prediction_error`. Proposed heat is available only while it
is `ready`. Useful attributes:
- `input_quality` — for each room and each weather source (grouped by metric):
`status` (`valid`, `missing`, `unknown`, `unavailable`, `stale`,
`invalid_timestamp`, `future_timestamp` or `invalid_value_or_unit`),
- `input_quality` — per zone (with each of its sensors) and per weather source
(grouped by metric): `status` (`valid`, `missing`, `unknown`, `unavailable`,
`stale`, `invalid_timestamp`, `future_timestamp` or `invalid_value_or_unit`),
`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
reload and can read up to one poll, 5 minutes, low). Rooms also have `used`:
whether that room's reading was used in this poll.
- `excluded_rooms` and `weight_coverage` — rooms left out of this poll, and the
share of the total comfort weight still covered by the rooms used (1.0 = all).
reload and can read up to one poll, 5 minutes, low). Zones and sensors have `used`.
- `excluded_zones` and `weight_coverage` — zones left out of this poll, and the
share of the total comfort weight still covered by the zones 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
warm) and its error bound. Learning waits while the bound is above 0.1, for
example for about 30 minutes after an HA restart.
- `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`,
`relay_changed`, `outside_source_changed` or `lag_uncertain`.
- `models` — per-room coefficients, accepted sample counts and the in-sample
5-minute error (`mae_5m`), which is **not** a forecast accuracy.
- `retired_room_models` — removed rooms whose learned models are kept.
`sensors_changed` (a zone's set of usable sensors changed), `learned`,
`ventilation` (only window-affected zones; given to the ventilation model),
`partly_rejected` (an outlier in some zone), `interval_out_of_range`,
`relay_changed`, `outside_source_changed`, `lag_uncertain` or `waiting_for_inputs`.
- `models` — per-zone base coefficients, accepted sample counts and the in-sample
5-minute error (`mae_5m`, **not** a forecast accuracy), `model_in_use` and the
extended layer's coefficients with confidence.
- `retired_zone_models` — removed zones whose learned models are kept.
`input_quality` and `weather_comparison` change every poll, so they are not
written to the Recorder; the other attributes are.
`input_quality`, `weather_comparison`, `heat_sources`, `windows`, `ventilation`
and `ventilation_events` change every poll, so they are not written to the
Recorder; the other attributes are.
### Choosing freshness limits
Room and weather report-age limits are set during setup and in **Configure**
(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
**left out**: the weighted temperature, learning and forecasts continue with the
other rooms, whose weights are re-normalized. Status shows it in `excluded_rooms`.
Learning and forecasts pause only when no room is usable, or when the relay,
thermostat or outdoor temperature is missing. A room that reports again learns
A zone sensor that is older than its limit, unavailable, missing or invalid is
**left out**; the zone uses its other sensors (without learning across that change).
A zone with no usable sensor is left out entirely: the weighted temperature,
learning and forecasts continue with the
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.
The checkbox **Keep using the last value of a room sensor that stopped reporting**
@@ -155,16 +217,17 @@ age limit.
## Forecast accuracy
The four **Forecast Error** sensors measure the thermal model against reality,
continuously and out of sample. At every poll the integration records each 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
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
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
executed. Each sensor's state is the mean absolute error (°C) over the last 7 days,
across all rooms. Attributes, all in °C except counts:
executed. All zones are replayed together, with the electrical heat and window
openings that actually happened, and the base model is scored on the same spans. Each sensor's state is the mean absolute error (°C) over the last 7 days,
across all zones. Attributes, all in °C except counts:
- `samples` — number of scored forecasts. Consecutive forecasts overlap, so they
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.
- `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.
- `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
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
model and survive restarts; deleting the model resets them.