# 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 1. **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. 2. **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. 3. **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. 4. **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. 5. **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. 6. **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](https://developers.home-assistant.io/docs/config_entries_index/), [config migration](https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#config-entry-migration), [entity registry](https://contributors.home-assistant.io/core/integrations/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](https://www.home-assistant.io/docs/configuration/state_object/). ## 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: 1. Relay changes are still inferred from `last_changed` at 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. 2. The lag estimate is not persisted, so an options reload restarts it from time-in-state (exact for a long-steady relay, approximate otherwise). 3. Learners of rooms removed in Options are dropped at the next save; re-adding a room, or reverting an accidental relay change, starts fresh. 4. Status attributes change every poll (`age_seconds`, MAE), so Recorder stores a new attribute row every five minutes. Consider `_unrecorded_attributes`. 5. 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.