Record findings, evidence, compatibility notes and remaining gaps in AUDIT.md; add user-facing upgrade notes to README and update test counts; note progress against Milestone 0 in ROADMAP. No version bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
15 KiB
Markdown
186 lines
15 KiB
Markdown
# 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.
|