Document second audit pass

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>
This commit is contained in:
buzeraandClaude Opus 5.5 committed 2026-10-26 21:21:43 +02:00
1 parent df69dc9645
commit af0a564279
3 files changed
+89 -1

No files matched your search

+48
View File
@@ -135,3 +135,51 @@ 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.