Stability and correctness after 0.4.0: unknown window states are no longer read as closed; learned coefficients are reset selectively when the configuration they were learned under changes; plans are discarded when an input changed during optimization, and an overtaken run never publishes; model accuracy (Model Error) is separated from the accuracy of published forecasts (new Forecast Error sensor). Base coefficients report confidence; sensor weights are labelled in the zone form. Still observation-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
400 lines
29 KiB
Markdown
400 lines
29 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.
|
||
|
||
|
||
## 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.
|
||
|
||
|
||
## 0.2.3 — stale-room policy
|
||
|
||
Decided by the user: pausing everything because one room sensor is unusable is
|
||
not acceptable for future control. An unusable room is now left out and the other
|
||
rooms' weights re-normalized; an opt-in checkbox keeps a stale sensor's last value
|
||
for sensors that report only on change. Two safeguards were added: the keep-last
|
||
option stops after 24 hours without a report, and Status exposes `excluded_rooms`
|
||
and `weight_coverage`, so a forecast based on a small share of the comfort weight
|
||
is visible.
|
||
|
||
Open for the control design: a minimum `weight_coverage` below which the MPC must
|
||
not decide, and the outdoor-temperature outage policy (still pauses). Simulated
|
||
two-hour bedroom outage: the other room kept learning (7 → 25 samples, previously
|
||
frozen at 7) and the bedroom resumed one poll after reporting again.
|
||
|
||
**Verification.** `sh scripts/check.sh` passes for HA 2025.12.0 and 2026.2.3 (69
|
||
offline tests, 18 HA Core tests). The offline tests also pass on Python 3.9.6.
|
||
|
||
|
||
## 0.3.0 — forecast evaluation
|
||
|
||
Scope: repository code and tests (HA 2025.12.0 and 2026.2.3 harness); no HAOS.
|
||
|
||
Design choices and why:
|
||
|
||
- **Score the thermal model, not the MPC plan.** Generic Thermostat controls the
|
||
relay, so the plan is never executed and cannot be checked. Each forecast is
|
||
replayed with the radiator-lag values that the relay's actual history produced,
|
||
which tests the model under known heat input.
|
||
- **Out of sample.** Each replay uses the parameters stored at the forecast start;
|
||
later learning never influences the forecast it is scored on.
|
||
- **Only known inputs.** Spans with irregular polls, an uncertain lag estimate, or a
|
||
missing relay/outdoor/room reading are not scored, and a missing shared input
|
||
restarts the window.
|
||
- **Baseline.** Persistence (no change) is scored on exactly the same spans; `skill`
|
||
reports the share of its error the model removes.
|
||
- **Known limits.** The outdoor temperature is held at its start value (as the MPC
|
||
does); consecutive forecasts overlap, so samples are not independent; the 7-day
|
||
window includes learning-phase forecasts.
|
||
|
||
Evidence: on the synthetic lagged plant (5 simulated days, thermostat cycling,
|
||
outdoor ±5 °C daily swing) the 60-minute error is 0.03 °C against 0.39 °C for
|
||
persistence (skill 0.92); the 120-minute error grows to 0.09 °C from the
|
||
constant-outdoor assumption. A model matching the plant exactly scores zero error;
|
||
a model without heat gain shows the expected negative bias. These are checks of
|
||
the evaluation itself, not of real-world accuracy.
|
||
|
||
Also in this release: per-room rejected-sample counters and per-outcome poll
|
||
counters (Milestone 0 "failed fitting counters"), a rollback contract test against
|
||
the 0.2.1 reader, ruff bug-finding lint, and removal of tracked `.DS_Store` files.
|
||
|
||
User feedback during testing: after a reset the Status entity only said
|
||
`learning`, and with Developer Tools moved under Settings in recent HA versions the
|
||
attributes were hard to find, so learning activity was invisible for the first ~30
|
||
minutes. Two sensors now show it directly: Learning Progress (%) and Last Sample
|
||
(an enum with translated states, including *Waiting for radiators to settle*). A
|
||
poll with missing shared inputs now reports `waiting_for_inputs` instead of nothing.
|
||
|
||
**Verification.** `sh scripts/check.sh` passes in both environments: lint, 80
|
||
offline tests, 20 HA Core tests, compilation, package checks. The offline tests
|
||
also pass on Python 3.9.6.
|
||
|
||
|
||
## 0.4.0 — zones, electrical heat, ventilation, wind, inter-zone exchange
|
||
|
||
Scope: repository code and tests (HA Core 2025.12.0 and 2026.2.3 harness); no
|
||
HAOS, device or deployment testing. Still no actuator code; the static and runtime
|
||
service-call guards pass.
|
||
|
||
Design decisions and why:
|
||
|
||
- **Zones as config subentries.** HA's native pattern for several items under one
|
||
integration gives per-item add/edit/delete and removes a zone's entities with it.
|
||
Config entry version 3; migrating from 2 keeps each room's sensor ID as the zone
|
||
key so learned models, entities and statistics carry over. Rollback needs the
|
||
backup.
|
||
- **Layered model per zone.** The 0.3 base model is kept unchanged as fallback and
|
||
as the comparison every forecast is scored against; the extended and ventilation
|
||
layers learn only from intervals where their inputs are known, start at 0 with
|
||
priors sized in "typical samples", and report confidence. This follows the
|
||
roadmap rule to retain the simpler model and justify complexity by measured
|
||
improvement.
|
||
- **Ventilation separated from envelope loss.** Open, unknown or recovering windows
|
||
exclude a zone from base and extended learning; a separate learner fits the
|
||
residual. On the synthetic week, not doing this produced a −0.013 °C/5 min bias
|
||
and an 11 % inflated boiler coefficient.
|
||
- **Baseline-centred electrical heat.** The persistent load is already in the bias
|
||
term; only variation above a 24-hour 20th percentile is modeled, after a
|
||
first-order response driven by the power held over each interval.
|
||
- **Bounded directional wind term.** |vent_wind_dir| ≤ vent_wind, so leeward
|
||
windows still ventilate; angles use wrap-around differences.
|
||
- **Forecast assumptions** are single, conservative scenarios (electrical heat
|
||
fades with a half-life, open windows close after a set time) and are
|
||
configurable; no uncertainty band yet.
|
||
- **Exact compiled step** for the plan search: all terms are affine in temperatures
|
||
and lag, so coefficients are read off the real model (verified to 1e-12); the
|
||
180-minute search is 4.5× faster.
|
||
|
||
Evidence (synthetic apartments, 60-minute error, full vs base vs no change): gaming
|
||
office 0.031 / 0.182 / 0.40 °C; ventilation bedroom 0.026 / 0.218 / 0.52 °C; two
|
||
coupled rooms 0.024 / 0.037 / 0.29 °C. Coefficients recovered: electrical, boiler
|
||
and loss within 10 %; combined natural+wind ventilation within 15 %; recovery within
|
||
15 %; wind direction within 30 %. Coupling is found but correctly reported as not
|
||
identified when the rooms' difference is steady.
|
||
|
||
Not done, by decision: learned response time constants (configurable; compare
|
||
candidates out of sample next), a wall-mass state, forecast scenarios/uncertainty,
|
||
weather forecasts. Learners of removed zones keep only the base model.
|
||
|
||
**Verification.** `sh scripts/check.sh` passes in both environments (lint, 125
|
||
offline tests, 26 HA Core tests, compilation, package/translation/changelog checks).
|
||
The offline tests also pass on Python 3.9.6.
|
||
|
||
## 0.4.1 — stabilization after 0.4.0 — 2026-10-27
|
||
|
||
Scope: correctness of 0.4.0, no new features. Repository code
|
||
and tests only (HA Core 2025.12.0 and 2026.2.3 harness); no HAOS, device or
|
||
deployment testing. Still no actuator code; the static and runtime service-call
|
||
guards pass.
|
||
|
||
Confirmed issues and fixes:
|
||
|
||
1. **Unknown window states read as closed** (2bddc74). Reproduced: an open window
|
||
whose sensor became unavailable ended the ventilation event, started recovery
|
||
at full weight and let the forecast assume a closed window. Windows are now
|
||
tri-state with a remembered last known state: last seen open → possibly open
|
||
(base and extended layers do not learn, the forecast assumes open); a close
|
||
while unknown is logged as unconfirmed with its longest possible duration;
|
||
recovery is learned only after a confirmed close. Seven transition tests
|
||
(open/closed/unknown in every order, restart while unknown, polarity).
|
||
2. **Learned coefficients reused after incompatible changes** (099c63c, 8eef04c).
|
||
By construction: changing a zone's sensors or weight ratio, a heat source, a
|
||
window or the recovery time kept every coefficient. New
|
||
`compatibility.py` fingerprints each component's configuration; on restore
|
||
only changed components are reset (a measurement change recalibrates the zone:
|
||
coefficients kept as a starting point, uncertainty, counts and accuracy
|
||
statistics reset). Comfort weight, target offset and names never invalidate.
|
||
Heat-source baseline samples are dropped when the effective power's definition
|
||
changes. 0.4.0 data has no fingerprints, is assumed compatible, and is
|
||
fingerprinted at the first start. Resets are logged in `model_changes`.
|
||
3. **Plans published from changed inputs** (d73e32a). The pre/post-optimization
|
||
check covered only temperatures, relay and thermostat, and an overtaken update
|
||
could still publish. The input snapshot now covers zone and outdoor
|
||
temperatures, electrical power, wind, windows, relay and thermostat, with
|
||
tolerances so sensor noise does not discard every plan; a run counter keeps an
|
||
older run from overwriting a newer result; shutdown is checked after saving
|
||
and after the worker; a per-entry storage lock serializes save, restore and
|
||
delete (also fixing a delete-during-save race that could recreate the file).
|
||
4. **Model accuracy reported as forecast accuracy** (26c415f). The Forecast Error
|
||
sensors replayed the model with the actual future relay, electrical heat and
|
||
windows. They are now **Model Error** (unique IDs unchanged) with RMSE,
|
||
effective samples and a quality label, and their state stays unknown below 5
|
||
effective samples. A new **Forecast Error** sensor scores published forecasts
|
||
with the plan and assumptions of the moment, only when the relay followed the
|
||
plan (≥ 12 of every 15 minutes in the planned state); otherwise the forecast is
|
||
counted as counterfactual, unverifiable or missing, never as error.
|
||
|
||
Also reviewed:
|
||
|
||
- **Confidence** (a44163a). Extended and ventilation coefficients already used
|
||
posterior variance. Base coefficients now report it too; a test shows two weeks
|
||
of non-exciting data (relay off, steady difference) keep `heat` at 0 and `loss`
|
||
and `bias` below 0.8, while four varied days bring all three above 0.8.
|
||
Readiness still needs sample counts; it is a gate, not a confidence.
|
||
- **Lifecycle** (4911b32). Three reloads leave one relay listener and one polling
|
||
coordinator; after unload nothing is recorded or published. The test was
|
||
checked to fail with the unsubscribe removed.
|
||
- **Sensor weights UI** (9bb64e7, user report). Per-sensor weights existed but
|
||
were an unlabelled second step; they are now numbered fields with the sensors
|
||
listed, in English and Romanian.
|
||
|
||
Remaining risks:
|
||
|
||
- In observation mode the relay follows the MPC plan mainly when Generic
|
||
Thermostat happens to agree, so Forecast Error may stay `insufficient` for a
|
||
long time, and the scored subset is biased towards those periods.
|
||
- `effective_samples` corrects for overlapping horizons and shared zones only; it
|
||
is not an autocorrelation estimate. Quality thresholds (5, 30) are judgement.
|
||
- Configuration changes made before this update cannot be detected: 0.4.0 models
|
||
are fingerprinted as they are. A sensor replaced under the same entity ID, or a
|
||
weather provider swap with a systematic offset, does not change any fingerprint.
|
||
- Input-change tolerances are fixed constants, not options.
|
||
- Published forecasts awaiting their horizon are lost on restart.
|
||
- None of this has run on HAOS or with real devices.
|
||
|
||
**Verification.** In both environments (HA 2025.12.0 and 2026.2.3, Python 3.13):
|
||
ruff, 159 offline tests, 28 HA Core tests; the offline tests also pass on Python
|
||
3.9.6. `sh scripts/check.sh` additionally validates packaging, translations and
|
||
the changelog.
|