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>
18 KiB
v0.2.1 source audit — 2026-10-26
Scope: repository implementation, offline tests and documented HA APIs. No HAOS, physical relay, thermostat, deployment or gas-consumption testing was performed. The manifest remains 0.2.1; these are unreleased reliability fixes, not a release acceptance declaration. Milestone 0 remains open.
Findings and repairs
| Area | Baseline finding | Repair / evidence |
|---|---|---|
| Lifecycle, high | Config flow VERSION=2 had no migration hook; the weather helper's legacy fallback alone cannot migrate HA entries. | Migrate v1 data and options independently through HA's update API, retaining options precedence and existing prioritized selections. Reject newer config versions. |
| Lifecycle, medium | Removed room forecast entities remained in the registry after options reload. | Remove only obsolete room forecast IDs for this integration and entry; retain all other entities and compatible IDs. |
| Lifecycle, medium | Raised platform setup left the coordinator in hass.data. | Remove that reference on failure. HA's own partially loaded platform cleanup still requires real runtime testing. |
| Persistence, high | Malformed roots/counters could crash restore; finite but nonphysical coefficients and invalid covariance could be accepted. | Validate shapes, coefficient bounds, symmetric positive-definite covariance, integer counts and finite error totals; reset malformed rooms atomically. |
| Learning, high | Global ON/OFF counts increased even when fitting rejected outliers and could qualify a newly added room using other rooms' history. | Persist accepted ON/OFF counts per room; readiness requires every room to qualify. Retain compatible legacy coefficients/counts but collect fresh excitation evidence. |
| Learning, high | A 4–7 minute interval was treated as exactly 5 minutes, including intervals containing relay transitions. | Accept 270–330 seconds, normalize temperature change to five minutes, compute lag using actual elapsed time; reject gaps/backward time, source changes and relay changes since the previous poll (including on/off round trips). |
| Inputs, high | Thermostat targets were interpreted as Celsius even in a Fahrenheit HA installation. | Normalize using thermostat temperature_unit when present, otherwise HA's configured temperature unit. |
| Inputs, medium | Invalid weights or malformed unit metadata could cause crashes/nonfinite arithmetic. | Reject invalid weights with unavailable proposal; reject non-string units as unusable sources. |
| Prediction, high | Inputs could change while saving or optimizing, yet the old plan could be published as ready. | Compare captured HA State snapshots before publishing; discard results when any watched state changes. |
| Prediction, medium | A 30/45-minute pure optimizer horizon returned its endpoint as a 60-minute forecast. | Return no 60-minute forecast for horizons below 60. UI already limits horizons to at least 60. Replay tests check the actual +60 and endpoint values. |
| Prediction, medium | NaN weights/lag and negative/nonfinite optimizer parameters were not rejected consistently. | Add finite/range validation and regression tests. |
The initial 10 tests passed. The expanded suite contains 33 offline tests covering model/weather logic plus coordinator and integration entry points using small HA boundary doubles. The doubles check our calls and state handling; they do not exercise HA scheduling, Store internals, entity registration or actual config forms. The test service-call spy raises if invoked. Source inspection found no actuator service calls, service registration, relay automations or network model inference.
Remaining gaps, in priority order
- Freshness and alignment: numeric but stale room/outdoor states are still accepted. Define configurable freshness limits and reporting timestamps in the UI, distinguish unchanged values from missing reports, expose rejection reasons, and test fallback/recovery. Missing units still assume canonical units for compatibility. Optional weather source lists lack uniform maximum-size checks.
- Real HA test harness and HAOS acceptance: config/options form submission, reload listener/poller teardown, disabled entities, partial platform setup failure, actual storage failures, restart/uninstall and registry cleanup need tests against a declared supported HA version. Add CI, translation/schema and packaging checks. Existing en/ro flow strings are unchanged by this patch.
- Sampling and identification: polling detects a relay change through last_changed and skips that interval; it does not integrate the relay duty cycle. Restart/gaps reset the lag proxy to the current command, which is only an approximation. Use timestamped relay events and aligned sensor observations before claiming robust radiator identification. Rejecting transition intervals can slow learning. The jitter correction is a local linear approximation.
- Persistence and identity: Store envelope version stays 1; the additive model payload is schema_version=2. Legacy payloads remain readable. Unsupported payload schemas are ignored; comprehensive future-version downgrade protection remains open. Renamed room entity IDs are treated as replacements (fresh model), not automatically matched to entity-registry identity. Same-ID physical sensor relocation and systematic weather provider offsets remain undetected.
- Forecast validity: forecasts remain counterfactual MPC plans with constant outdoor temperature. Five-minute residual MAE is not held-out multi-horizon accuracy. No actual-policy forecast, recorded trajectories, confidence estimate, identifiability test or persistence benchmark exists. Prediction updates remain five-minute polls; a later manual change does not instantly refresh the diagnostic.
- Resource use: optimization is offloaded, but no Pi 5 benchmark exists. Model state is written on each eligible interval (roughly 288 writes/day). Rejected-fit reasons and aggregate failure counters are not yet exposed.
Compatibility and safe trial
Back up HA before installing. Replace the component directory and restart Core; keep Generic Thermostat as the only relay writer. Compatible room coefficients and forecast unique IDs remain stable. Older saved models need at least 30 newly accepted ON and 30 OFF samples per room before becoming ready again, plus the existing 200-total-sample condition. No artificial heating cycles are generated.
Observe status, normalized target, per-room model counts and weather provenance. Test manual heat/off and preset changes, a missing sensor, an options reload, room removal and a restart. The proposed-heat entity must remain diagnostic only. Use a disposable config entry for uninstall testing because deletion removes its models. For rollback, restore the component and HA backup together; older code ignores the new per-room counters and retains its original defects. No installation was done as part of this audit.
Verification commands
python3 -m unittest discover -s tests -v— 33 tests pass (Python 3.9.6).PYTHONPYCACHEPREFIX=/tmp/heating-mpc-audit-pycache python3 -m compileall -q custom_components/heating_mpc— passes.git diff --check— passes.
The initial unredirected compile check failed because macOS's Python cache path was outside the writable sandbox; the temporary-cache rerun succeeded. No full HA suite, type checker, linter, HAOS or physical-device checks were run.
API references used to check migration and entity-registry calls: HA config entries, config migration, entity registry.
Follow-up: configurable freshness
The freshness portion of finding 1 is now addressed: room/weather report-age limits, stale-source fallback, recovery without cross-gap learning and per-source quality reasons are implemented. Defaults are 30/120 minutes and both limits are editable in setup/Options with English and Romanian labels. This supersedes the original audit's statement that stale numeric readings are accepted. Physical measurement timestamps, source alignment, cached-provider detection and HAOS validation remain open. See README for compatibility and safe trial details.
The suite now has 42 passing offline tests. Compilation with the temporary cache
and git diff --check pass. The API basis is HA's
state timestamp documentation.
Follow-up: HA Core harness and CI
Added a separate tests/ha pytest suite using the real Home Assistant 2025.12.0
package and matching fixtures, pinned for Python 3.13. All 14 tests pass locally,
alongside the existing 42 offline tests. The full scripts/check.sh checks and
pip check passed. The suite exercises the actual config manager, config/options
schemas, registry, scheduler and Store API, with mocked disk I/O and simulated
sensor/reference entities. A service-call guard covers every test. No actuator
calls were observed. Tests include cold setup with stored data/manual off, but not
a full HA process restart or HAOS hardware validation.
Added a Forgejo/Gitea Actions workflow and lightweight package/translation checks. Remote runner execution, a modern-HA version matrix, dedicated lint/type checks, full process restart, partial platform failure and real disk-failure testing remain open. The test harness needed a pycares 4.11.0 pin for compatibility with HA's DNS dependency. Integration runtime dependencies and behavior are unchanged.
CI provisioning correction
Remote CI reached checkout on the dedicated bs-node runner but failed at
setup-python: no Python 3.13 x64 build matched the Debian Bookworm job OS. No tests
ran in that job. The workflow now uses setup-uv v6 with uv 0.9.6 to create a seeded
Python 3.13 virtual environment, then runs the same dependency and test checks.
GitHub cache services are disabled and no Gitea token is supplied to setup-uv.
The runner label and integration runtime are unchanged. Remote execution of the
corrected workflow remains to be verified.
Follow-up: second source audit — lag, learner stability, options
Scope as above: repository code, offline tests and the HA Core 2025.12.0 suite. No HAOS, physical relay, thermostat or deployment testing was performed. The manifest remains 0.2.1 and no actuator code was added.
| Area | Finding | Repair / evidence |
|---|---|---|
| Learning and prediction, high | The radiator-lag proxy was reset to the new relay command at every relay change and after any skipped interval: it read 1.0 straight after ON and 0.0 straight after OFF, so the lag model was effectively disabled around every boiler cycle. On a synthetic lagged plant cycled by a hysteresis thermostat, the original code learned a heat gain 34 % low (0.079 vs 0.12) and a loss coefficient 43 % low. The optimizer also started each post-transition plan from the wrong lag. | Lag is tracked at every poll with a valid relay state, independently of room sensors. One change between polls is integrated at its last_changed time. Ambiguous cases (on/off round trip between polls, relay outage, restart, clock jump) restart from time-in-state with an explicit error half-width; learning waits until it is at most 0.1 (about 32 minutes after an HA restart). The same synthetic test now learns 0.1195 / 0.00389 (true 0.12 / 0.004). |
| Learning and persistence, high | RLS covariance windup: with forgetting 0.999 and no heat excitation (summer, long OFF periods) the heat variance grew by 1/0.999 per sample without limit, about 8.7×10¹² after 104 simulated OFF days. The first ON samples afterwards could swing coefficients to their bounds, and accumulated asymmetry (9×10⁻⁹ there) approached the absolute 10⁻⁸ restore tolerance, beyond which a restart would silently reset the model. | Covariance is symmetrized and its diagonal bounded by the prior after every update; row/column scaling preserves positive definiteness. Restore uses a relative round-off tolerance and applies the same bound, so wound-up older models load rather than reset. |
| Configuration, medium | Optional weather source fields used the current selection as a form default. A cleared field omitted from the submission re-inserted the old sources, and an options key absent from the submission fell back to the original setup data, so optional providers could not be removed in the UI. | Suggested values pre-fill the form instead; omitted optional lists are saved as empty and override older entry data. Covered by an HA Core options-flow test. |
| Prediction availability, medium | A finished plan was discarded when any watched State object changed during the calculation, including diagnostic-only weather sensors and the thermostat's own current_temperature/hvac_action. The proposed-heat entity could go unavailable for five minutes without cause. |
Compare only values the prediction uses: room and outdoor-temperature states and units, thermostat mode and target, relay state and last change. |
| Lifecycle, low | The coordinator relied on HA's config-entry ContextVar; HA reports this, though it does not enforce it for custom integrations. | Pass config_entry explicitly. |
| Tests, medium | The shadow-only rule was checked only at runtime on exercised paths. | Static AST tripwire over the integration source: no service calls or registration, no actuator method names or service-name strings, and only sensor/binary_sensor platforms. |
New Status attributes: heat_lag, heat_lag_uncertainty and sample_result
(first_sample, learned, partly_rejected, interval_out_of_range,
relay_changed, outside_source_changed, lag_uncertain). They explain why the
sample counters are or are not increasing.
Compatibility. Persisted payload schema stays 2; existing models load and get
the covariance bound on load. Coefficients learned before this fix were fitted
with the wrong lag. With forgetting 0.999 they should re-converge over roughly
three to four days of heating data, but sample counts are kept, so ready does
not reflect this. Treat earlier forecasts as uncalibrated. For a clean model,
delete and re-add the integration (this deletes the learned data).
Remaining gaps found in this pass, in addition to those above:
- Relay changes are still inferred from
last_changedat each poll. An odd number of changes greater than one between polls is treated as one change. A read-only state-change listener for the relay would remove this. - The lag estimate is not persisted, so an options reload restarts it from time-in-state (exact for a long-steady relay, approximate otherwise).
- Learners of rooms removed in Options are dropped at the next save; re-adding a room, or reverting an accidental relay change, starts fresh.
- Status attributes change every poll (
age_seconds, MAE), so Recorder stores a new attribute row every five minutes. Consider_unrecorded_attributes. - The HA Core suite pins 2025.12.0; current HA releases are untested.
Verification. sh scripts/check.sh in a Python 3.13 environment with
requirements-test.txt: pip check, 56 offline tests, 16 HA Core tests,
compilation, package/translation validation and git diff --check all pass.
python3 -m unittest discover -s tests also passes on Python 3.9.6. The new
synthetic-plant and windup tests were also run against the original code to confirm
they fail there.
0.2.2 release follow-up
Scope as above; HA Core tests now run on 2025.12.0 and 2026.2.3 (Python 3.13). No HAOS, device or deployment testing. Still no actuator code.
| Area | Finding | Repair / evidence |
|---|---|---|
| Learning, medium | Polling saw only the relay's current state and last change. Two or more changes between polls (an on/off round trip) left the lag unknown and paused learning for about 30 minutes each time. | Read-only async_track_state_change_event listener records each change; all changes between polls are integrated. HA delivers these events one loop iteration late, so the current state stays authoritative and fills in a change not yet recorded. HA Core test: a round trip keeps the error bound below 0.05 (0.46 with the listener disabled). |
| Lifecycle, medium | HA runs unload callbacks for ConfigEntryNotReady/ConfigEntryError but not for unexpected exceptions during setup, so anything registered with async_on_unload before such an error would leak. |
Setup removes the listener and coordinator itself on any failure and registers the listener for unload only after success. Offline test with a failing platform setup. |
| Persistence, medium | Saving wrote only configured rooms, so removing a room in Options (even briefly) discarded its model. | Validated models of removed rooms are kept (at most 24) under an optional retired_learners key and restored on re-adding. HA Core options round-trip test. Payload schema_version stays 2; v0.2.1 was checked by hand to read the new payload. |
| Diagnostics, low | Choosing freshness limits required knowing each sensor's report interval, which HA does not show. | max_age_seconds_24h per source (largest age at polls, hourly buckets, reset on reload). |
| Resources, low | input_quality changes every poll (report ages), creating a new Recorder attribute row every five minutes. |
input_quality and weather_comparison marked unrecorded; HA Core test checks state_info. |
| Compatibility | Only HA 2025.12.0 was tested. | Added the newest published fixtures (HA 2026.2.3) as a CI matrix entry; all checks pass locally with no deprecation reports for this integration. Newer HA releases remain untested. |
Not changed, by decision: a stale room still pauses learning and forecasts for all
rooms (needs a policy decision); the lag estimate is not persisted (an HA restart
resets the relay's last_changed, so continuity cannot be verified anyway).
Verification. sh scripts/check.sh passes in both test environments (pip
check, 65 offline tests, 18 HA Core tests, compilation, package/translation/changelog
checks, git diff --check). The offline tests also pass on Python 3.9.6.