Bump the manifest to 0.2.2 and add CHANGELOG.md with upgrade, reset and rollback notes covering all reliability work since 0.2.1. The README drops its accumulated "Unreleased" sections in favor of the changelog and gains a Diagnostics section explaining the Status attributes and how to choose freshness limits. The package check now fails if the manifest version has no changelog entry. ROADMAP marks the Milestone 0 items now backed by tests and resets the next-session backlog; AUDIT.md records this round's findings. Still observation-only. Tested in the HA Core harness with HA 2025.12.0 and 2026.2.3; not on HAOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
160 lines
11 KiB
Markdown
160 lines
11 KiB
Markdown
# Heating MPC 0.2.2 — 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.
|
||
|
||
This project targets Home Assistant OS on a Raspberry Pi 5, with one existing ON/OFF central-heating relay, `generic_thermostat`, and individual indoor temperature sensors.
|
||
|
||
## Install / upgrade
|
||
|
||
1. Create a Home Assistant backup.
|
||
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.
|
||
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.
|
||
|
||
**No entity IDs are hard-coded.** Every room and outdoor measurement can be changed later via **Configure**, without editing YAML or Python.
|
||
|
||
## 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 |
|
||
| Cloud cover | No | No, diagnostics/source comparison only |
|
||
| Humidity | No | No, diagnostics/source comparison only |
|
||
| Feels-like temperature | No | No, diagnostics/source comparison only |
|
||
| Atmospheric pressure | No | No, diagnostics/source comparison only |
|
||
| Visibility | No | No, diagnostics/source comparison only |
|
||
| Rain / snow | No | No, diagnostics/source comparison only |
|
||
|
||
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).
|
||
|
||
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.
|
||
|
||
## 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.
|
||
|
||
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.mpc_outdoor_temperature` — selected external temp and alternatives.
|
||
- Per-room predicted temperatures after 60 minutes.
|
||
- `binary_sensor.heating_mpc_proposed_heat` — **hypothetical** heating demand (unavailable until ready).
|
||
|
||
## 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.
|
||
- 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.
|
||
- The demand switch is not a burner-flame or real gas consumption measurement.
|
||
- Not yet tested on HAOS or with real devices. Automated tests run the integration in a Home Assistant Core test harness (HA 2025.12.0 and 2026.2.3) with simulated entities.
|
||
- Home Assistant Core APIs and config-flow selectors can change; take a backup before trying this experimental custom integration.
|
||
|
||
## Diagnostics
|
||
|
||
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:
|
||
|
||
- `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`),
|
||
`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).
|
||
- `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.
|
||
|
||
`input_quality` and `weather_comparison` 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 older
|
||
than its limit pauses learning and forecasts for **all** rooms until it reports
|
||
again. A stale outdoor source falls back to the next one in its list; optional
|
||
weather metrics never block. Recovery starts a new sampling interval, so nothing is
|
||
learned across an outage.
|
||
|
||
Within the limit, the last reading counts as current, so a too-generous limit lets
|
||
a frozen value be learned for a while. A too-tight limit pauses the integration
|
||
whenever a sensor is quietly between reports. Freshness uses HA's `last_reported`:
|
||
the time a sensor last reported to HA, even with an unchanged value. It is not the
|
||
age of the physical measurement, so a provider re-publishing cached data looks
|
||
fresh, and sensors that report only on change can look stale in a steady room.
|
||
|
||
After a day of running, set each limit comfortably above (about 1.5–2×) the largest
|
||
`max_age_seconds_24h` among the sensors it covers. The relay and thermostat have no
|
||
age limit.
|
||
|
||
## Release notes
|
||
|
||
See [CHANGELOG.md](CHANGELOG.md) for changes and upgrade notes per version, and
|
||
[AUDIT.md](AUDIT.md) for audit findings and test evidence.
|
||
|
||
## Developer checks and Forgejo/Gitea Actions
|
||
|
||
The HA Core test suite runs against **Home Assistant 2025.12.0** (the minimum in
|
||
`hacs.json`, `requirements-test.txt`) and **2026.2.3** (newest published test
|
||
fixtures, `requirements-test-latest.txt`), both on Python 3.13. Use a dedicated
|
||
development virtual environment; these test dependencies are not installed with
|
||
the integration and do not change its local, standard-library model inference.
|
||
|
||
```bash
|
||
python3.13 -m venv .venv
|
||
. .venv/bin/activate
|
||
python -m pip install -r requirements-test.txt # or requirements-test-latest.txt
|
||
sh scripts/check.sh
|
||
```
|
||
|
||
The check script runs dependency consistency checks, the offline tests, the HA
|
||
Core tests, compilation, package metadata, translation and changelog checks, and
|
||
diff whitespace checks. The 2025.12.0 requirements pin a compatible DNS library
|
||
because pycares 5 breaks that release's dependency. The script also disables a
|
||
transitive HA test dependency's optional metadata download. The offline tests also
|
||
run without Home Assistant installed:
|
||
|
||
```bash
|
||
python -m unittest discover -s tests -v
|
||
```
|
||
|
||
HA tests cover setup, config/options forms, migration, reload and entity cleanup,
|
||
scheduled sampling and poll cancellation, the relay listener, stored model
|
||
restoration, retention and deletion, missing inputs, manual off/presets, optimizer
|
||
failure and Recorder exclusions. A fixture rejects all HA service calls throughout
|
||
each test, and a static test fails if actuator service calls are added to the
|
||
source. HA Core runs locally with simulated input entities and mocked storage I/O;
|
||
this does not validate HAOS, physical devices, disk failures or full process restarts.
|
||
|
||
`.gitea/workflows/tests.yml` runs the same checks for both HA versions on pushes
|
||
and pull requests, on the dedicated `bs-node` Actions runner label
|
||
(`bs-node:docker://node:22-bookworm`). Repository Actions must be enabled, and the
|
||
runner must support the checkout and setup-uv actions. CI uses pinned uv 0.9.6 to
|
||
create a Python 3.13 virtual environment from standalone Python builds;
|
||
`setup-python` cannot provision this version in the Debian Bookworm job image.
|
||
Action caching is disabled to avoid requiring GitHub cache services on Gitea.
|
||
Dependencies, Python builds and action downloads need internet access in CI;
|
||
model inference does not. Remote CI execution has not yet been verified; local
|
||
validation used macOS.
|