Configure now opens a menu: Settings (the previous form) or Reset learned
model. After an explicit confirmation the entry is unloaded, the learned
parts of the stored model (zone, extended and ventilation coefficients,
models kept for removed zones, Model Error and Forecast Error statistics,
fingerprints) are removed under the storage lock, and the entry is set up
again. Configuration, heat-source baselines and the window event log are
kept, and model_changes records the reset. Unloading first means no update
still running can write the old model back. English and Romanian strings;
the gust hint now refers to Model Error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home Assistant marks a coordinator shut down only after the platforms are
unloaded, and the model save checked for shutdown before waiting for the
storage lock. A deletion could therefore be followed by a save that had
been waiting for it, recreating the file. Also, unloading cancels a running
scheduled update, which released the lock while the file write continued
in a worker thread.
- The coordinator is marked closing before platforms unload (restored if
unloading fails); closing updates, relay callbacks and saves do nothing.
- The save checks closing and a removed-model mark after acquiring the
lock, and runs shielded so the lock is held until the write finishes.
- Removal marks the model removed under the lock before deleting it.
- A failed setup also shuts the coordinator down.
Tests: unload and reload during a running optimization, removal during a
save, a save waiting for a deletion, simultaneous saves, repeated
setup/unload, shutdown ordering and failed setup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
README: window unknown states, what configuration changes do to the learned
model, inputs_changed tolerances, coefficient confidence, model_changes, and
Model Error vs Forecast Error. docs/MODEL.md: configuration-change table,
confidence semantics, both validation modes. CHANGELOG gains an Unreleased
section (version unchanged); AUDIT and ROADMAP record findings, evidence and
remaining risks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flipped contact sensor reverses what every term saw as open, including the
other zones' cross-ventilation term, whose fingerprint covered only the
window's sensor and opening factor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three reloads in a row: a relay change reaches only the current coordinator,
replaced coordinators neither learn nor publish, and after unload nothing is
recorded or published. Checked to fail when the listener is not unsubscribed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The base layer (loss, heat, bias) showed values only, so a zone with weeks of
samples looked equally trustworthy whether or not the data ever varied its
inputs. Each base coefficient now reports a confidence from its own posterior
variance, as the extended and ventilation layers already did: it stays at 0
for `heat` while the relay never cycles and below 0.8 for `loss` and `bias`
while the indoor-outdoor difference is steady. Readiness is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Forecast Error sensors replayed the model with the relay, electrical heat
and window history that actually followed, so they measured the model, not a
forecast anyone could have made. They are renamed Model Error (unique IDs
unchanged) and gain RMSE, effective sample count and a quality label; their
state stays unknown until there are at least 5 roughly independent spans.
A new Forecast Error sensor scores forecasts as published, with the plan and
assumptions of the moment they were made. A forecast is scored only when the
relay followed its plan; otherwise it is counted as counterfactual, not as
error. Statistics are saved with the model and dropped with a zone's
recalibration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirmed: the check run after the optimizer compared only zone sensors,
outdoor temperature, thermostat and relay. A window opening, a UPS load
change or a wind change during the calculation went unnoticed; nothing
prevented an older update that finished late from overwriting a newer
result; and an update in flight during unload still saved.
- Every input a prediction uses is captured as a normalized snapshot
(zone and outdoor temperatures, wind speed/direction/gust, heat-source
power, window states, relay state and switch time, thermostat mode and
target) and compared after the calculation with tolerances (0.25 °C,
50 W, 1 m/s, 2 m/s gusts, 20° direction around the circle; exact for
relay, thermostat and windows). A meaningful change discards the plan
and lists the inputs in changed_inputs; noise does not. No immediate
recalculation: an off-schedule poll would break the 5-minute learning
interval; the next regular poll computes a fresh plan.
- Updates are numbered; one overtaken by a newer update, or running when
the entry unloads, keeps the newer data and skips saving and
optimizing. Configuration changes reload the entry, so they are covered.
- A per-entry storage lock makes a reloaded coordinator restore only
after the previous one's save finished, and integration removal waits
for it too, so a late save cannot recreate a deleted model file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirmed: learned models were matched only by zone key, so replacing a
zone's sensors, changing sensor weights, moving or replacing a heat
source or turning a window kept coefficients learned for a different
configuration, silently.
compatibility.py fingerprints exactly what each learned component
depends on: the zone measurement (sensors and relative weights; equal
scaling changes nothing), each zone's heat sources, the neighbour's
measurement, the zone's windows, window orientation (direction term
only), the recovery time (recovery only) and other zones' openings
(cross term only). Comfort weight, target offset and names are not
included. Heat-source baselines have their own fingerprint (sensor and
included sources).
On restore, a changed measurement recalibrates the zone: base and
extended loss/heat/bias keep their values as a warm start, but their
uncertainty, counts and readiness start over, other terms return to the
prior, and the zone's forecast statistics are dropped. Otherwise only
the changed terms are reset; everything else, including other zones,
is kept. Each recalibration is logged (Status model_changes, info log)
and saved at once. Data from before fingerprints (0.4.0) is assumed to
match its current configuration and is fingerprinted at the first
start, so the next edit is detected even before a learned sample.
Tests cover comfort changes, sensor replacement and weights, window
orientation and size, moved and replaced heat sources, re-added zones,
0.4.0 data and a second restart, plus an HA Core test through real
subentry updates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sensor weights already existed, but only as a second step after saving
a zone with two or more sensors, with fields labelled by raw keys such
as weight__sensor.living_a, so they were easy to miss.
The zone form now says that choosing two or more sensors leads to a
sensor-weight step. That step uses numbered fields ("Sensor 1 weight",
English and Romanian) and its description lists which sensor is which
by friendly name. Editing a multi-sensor zone shows the saved weights.
Stored data is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Confirmed: when an open window's contact sensor became unknown or
unavailable, the tracker ended the ventilation event as a close at the
poll time, started recovery at full weight and forecasts assumed the
window closed. (Learning was already excluded for that zone.)
Each window is now OPEN, CLOSED or UNKNOWN with its last known state,
since when, and since when it is unknown:
- an unknown sensor keeps an open window's event (and the zone's
"open" status) going; forecasts use the last known state;
- a close observed only after an outage is recorded as unconfirmed:
closed=None with the reported time and a maximum duration; recovery
is assumed from that report but not used for any learning;
- opening seen only after an outage starts an event without a
temperature-before, so its recovery is not learned either;
- events count minutes with an unknown sensor; Status shows each
window's last known state and unknown minutes;
- one unknown sensor only blocks learning for its own zone.
Predictions with an unavailable wind sensor now keep an open window's
natural ventilation (calm air) instead of dropping the term; learning
still refuses unknown inputs.
Regression tests for every OPEN/CLOSED/UNKNOWN transition, a six-hour
outage, multiple windows with one unknown, and missing wind during
ventilation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zones, electrical heat sources, windows with ventilation, wind exposure
and recovery, and exchange between adjacent zones, all observational.
docs/MODEL.md documents the equations, units, coefficient meanings,
priors and bounds, learning conditions, forecast assumptions, validation
method and results, and limitations. README gains a configuration guide
for zones, heat sources and windows, the new diagnostics and the
accuracy comparison against the base model. CHANGELOG has upgrade notes
(config entry version 3, rollback needs the backup, renamed Status
attributes). ROADMAP marks the Milestone 2 items now covered; AUDIT
records the design decisions and evidence. AGENTS.md lists the new
modules and the convention that new model terms start at zero and must
beat the base model out of sample before being relied on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting the only zone in the UI reloaded the entry with no zones, and
min() over an empty set of models raised during setup, so the entry
failed to load. Counts and learning progress now default to 0, and the
status reads "no_zones" until a zone is added again; adding one resumes
normally. HA Core regression test included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single 5-minute +700 W spike moves the room's thermal response by at
most 0.2 kW and leaves the baseline at the persistent load. In calm air
the wind terms vanish while natural ventilation remains. Extended and
ventilation coefficients and heat-source baselines survive a restart
unchanged (no hour of baseline re-learning), and enabling an extra
ventilation term later keeps everything learned for the others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validation: the walk-forward evaluator now replays all zones together
with the full model (electrical heat, neighbour exchange, ventilation
and recovery) using the parameters frozen at the forecast start and the
electrical heat and window openings that actually happened, and scores
the base 3-parameter model on the same spans. Forecast Error sensors
add base_model_mae and improvement_vs_base (share of the base model's
error the new features remove; 0 = no gain, negative = worse). Saved
0.3 statistics load with their own error as the base column.
On synthetic apartments, 60-minute error full vs base: gaming office
0.031 vs 0.182 °C (83 % better), bedroom with ventilation 0.026 vs
0.218 °C (88 %), two coupled rooms 0.024 vs 0.037 °C (34 %; gone by
120 min, where the constant-outdoor assumption dominates). The same
ventilation week without window sensors shows no improvement, as it
should. These check the method, not real-world accuracy.
Performance: every term of a zone's 5-minute change is affine in the
zone temperatures and the radiator lag, and which layer applies depends
only on which inputs exist. compile_step_model reads the coefficients
off the real model per zone and step, so the plan search does a few
multiply-adds per step. It matches the direct model to 1e-12 and makes
a 180-minute, 5-zone search 4.5x faster (0.73 s here, close to the old
base-only search).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each zone forecast entity now shows the current temperature, the
forecast at +15/+30/+60/+120 minutes (what the horizon covers), which
model layer is in use, and what drives the zone right now in °C/h:
envelope loss, boiler, bias, electrical heat, neighbours, ventilation
and recovery, plus each layer's coefficients with standard error,
confidence and whether it is identified. These attributes change every
poll and are not recorded.
Tests for inter-zone exchange on a two-room synthetic apartment (living
room and a colder bedroom, one boiler): with a true coupling of 0.01 the
learner finds a clearly positive value but, because the difference
between rooms is fairly steady and therefore collinear with each room's
bias, correctly reports it as not identified; with no coupling it stays
near zero.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a window adds heat exchange that the envelope model mistook for
poor insulation or a constant cooling bias.
windows.py adds windows as config subentries ("Add window"): a contact
sensor (optionally inverted; unknown/unavailable stays unknown, never
"closed"), its zone, the direction its outside faces (default 345°,
editable) and an opening factor. Each poll computes the share of the
interval each window was open (one change located by its timestamp, a
round trip is unknown), the wind exposure with proper circular angles
(meteorological "from" direction vs the window's facing) and a recovery
weight after closing that decays towards 0, pulling the air back
towards its pre-ventilation temperature. A bounded event log records
opening/closing, duration, temperatures before/lowest/at close/30 min
after, weather and other open windows.
While a zone's window is open, unknown or recovering, the base and
extended models skip it; a separate ventilation learner fits the
closed-window model's residual with natural exchange, wind speed, a
directional term bounded by the wind term (a leeward window still
ventilates), recovery, and optional gust excess and cross-ventilation
terms. Forecasts assume an open window closes after a configurable time
(default 15 min), then recovers. New poll outcome "ventilation".
Weather gains optional wind direction (degrees, 360 -> 0) and wind gust
sources with the same priorities and fallback; knots and ft/s accepted.
On a synthetic week with ventilation events every four hours and a
daily outdoor swing, the configured zone keeps loss/heat/bias close to
the truth and recovers the combined natural+wind effect within 15 %,
recovery within 15 % and direction within 30 %. The same data without
the window configured produces a -0.013 °C/5 min cooling bias and an
11 % inflated boiler effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Electrical equipment heats its room, and a gaming session can add
hundreds of watts, which the 3-parameter model attributed to the boiler
or the bias term.
heat_sources.py adds heat sources as config subentries ("Add heat
source"): an active-power sensor (W/kW; VA and % rejected), the zone,
the share that becomes heat, an automatic baseline (20th percentile of
24 h, so the persistent UPS load is not double counted with the bias
term) or a manual one, an optional "included in" parent (the UPS total
minus a separately measured PC, one level deep) and a first-order
thermal response time. The heat during each interval is the power held
from its start, like the radiator lag. After startup or an outage a
source's response must run two time constants before it counts as
settled.
zone_model.py layers each zone: the base model is kept unchanged as the
fallback and comparison, and an extended FeatureLearner adds "internal"
(kW above baseline after the response) and neighbour coupling for
adjacent zones. It learns only intervals where all its inputs are
known, and is used for forecasts once as trained as the base. The
optimizer accepts a multi-zone step model; forecasts let extra power
fade to baseline with a configurable half-life (default 30 min).
On a synthetic office with thermostat cycling and gaming sessions the
extended model recovers the electrical, boiler and loss coefficients
within 10 %. Status lists each source's power, effective power,
baseline, variable and responding heat; models show the extended
coefficients with confidence. Saved models gain optional zone_models
and heat_sources keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each temperature sensor used to be its own "room", so a room with two
sensors counted twice and comfort importance was tied to sensors.
zones.py adds Zone: a name, one or more sensors with sensor weights, a
comfort weight, an optional target offset and adjacent zones (made
symmetric). Sensor weights only combine readings; comfort weights only
steer the MPC; neither touches the learned thermal coefficients.
Zones are config subentries (HA's native "Add zone" / edit / delete on
the integration page), with validation that a sensor belongs to one
zone and is not an outdoor source, and a sensor-weight step for
multi-sensor zones. Initial setup still asks for room sensors and
weights and creates one zone per sensor. Configure no longer asks for
rooms or weights.
Config entry version 3. Migrating from 2 turns each room sensor into a
one-sensor zone with its comfort weight and the sensor's entity ID as
the zone key, so learned models, forecast entities (now linked to their
zone, so deleting a zone removes them) and accuracy statistics carry
over. Older versions cannot load a version 3 entry; rolling back needs
the HA backup.
A zone learns only across polls that used the same sensors (a sensor
dropping out shifts the mean by its offset); the new poll outcome
"sensors_changed" reports this. Status renames: input_quality.zones
(per-sensor detail nested), excluded_zones, retired_zone_models; adds
zone_names. Invalid stored comfort weights now fall back to 1.0, so the
invalid_weights status is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Electrical heat, ventilation, wind and inter-zone exchange each need
their own coefficient next to the existing loss/heat/bias, and each must
stay physically bounded and not claim precision it does not have.
learning.py adds FeatureLearner: exponentially weighted RLS over named
features with per-feature bounds, priors at 0 for new effects, the same
covariance windup bound as the base model, and a linked bound that keeps
the directional wind term from cancelling more than the isotropic one.
Each prior is sized as "worth N typical samples" (20 for new effects,
200 for zone coupling, which is weakly identifiable). Confidence is the
share of a coefficient's information that comes from data; the standard
error uses a bias-corrected running noise estimate. Saved models are
matched by feature name, so adding a heat source, window or neighbour
retrains only that coefficient and keeps the covariance of the rest.
The existing 3-parameter learner now uses the same RLS step, covariance
bound and positive-definiteness check (behavior unchanged; all existing
tests pass).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
First part of Milestone 1: Forecast Error sensors scoring the thermal
model against reality, forecast trajectories, Learning Progress and Last
Sample sensors, rejected-sample and poll-outcome counters, plus lint
and rollback-contract tests. README explains forecast accuracy, the
30-minute learning wait after a restart and where Developer Tools now
lives. ROADMAP marks the items now backed by tests; AUDIT records the
evaluation design and evidence. Still observation-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During a first test the Status entity only said "learning" for the
first half hour (the radiator-lag wait after a restart), and the
attributes explaining why were hard to find. Add two sensors:
- Learning Progress (%): for the least trained room, the mean of its
three readiness requirements (200 samples, 30 ON, 30 OFF), each capped
at 100 %; 100 % means ready. Attributes show the counts.
- Last Sample: an enum of the last poll's learning outcome, with English
and Romanian state names (e.g. "Waiting for radiators to settle"). Its
history shows when learning happens.
Polls blocked by missing shared inputs now report waiting_for_inputs.
Readiness thresholds move to const.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finder metadata was tracked at the repository root and nearly got
packaged inside the integration. Ignore .DS_Store everywhere, untrack
the root copy (the local file stays) and make the package check reject
it under custom_components.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was no evidence of how accurate the thermal model is: the only
number was an in-sample 5-minute error.
At every poll, record each room's reading, the outdoor temperature, the
radiator-lag estimate and the model parameters. 15, 30, 60 and 120
minutes later, replay the room from that poll with the parameters it
had then (out of sample), the outdoor temperature held constant and the
radiator lag that actually followed from the relay's real history, and
compare with the reading now. A no-change forecast is scored alongside.
Only spans with regular polls and a well-known lag are scored. 7 days
of per-room statistics are saved with the model (optional "evaluation"
payload key; about 18 KB at most).
Four new sensors, Heating MPC Forecast Error 15/30/60/120 min, show the
mean absolute error with bias, persistence error, skill and per-room
detail. On the synthetic lagged plant the 60-minute error is 0.03 °C
against 0.39 °C for persistence.
Room forecast entities gain forecast_15min and plan_15min attributes
and are labelled counterfactual. Volatile attributes are unrecorded.
The model is now saved after scoring, so saved statistics are current.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Status showed accepted sample counts but not why learning was slow.
Each room's model now counts samples it rejected (outliers, implausible
inputs), persisted as an optional rejected_count key and shown as
models.<room>.rejected_samples. Status also counts every poll outcome
since start (sample_results_since_start).
Add tests that a model in the exact shape 0.2.1 wrote restores with its
coefficients, and that today's saved model still has the keys and types
0.2.1's restore code reads, so rolling back keeps the learned model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run ruff with pyflakes, syntax-error and bugbear rules only (no style
rules) over the integration, tests and scripts as part of check.sh and
CI. The codebase passes as is. Target Python 3.9 because the offline
tests are also run on older interpreters, which also leaves out the
zip(strict=) rule that needs 3.10. ruff 0.14.0 pinned in both test
requirement files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home Assistant 2026.3+ shows custom integrations' own brand images from
custom_components/<domain>/brand/. Add a radiator with a forecast curve
(solid past, "now" marker, dashed prediction) as icon.png/icon@2x.png
(256/512 px, transparent) plus dark-theme variants, so the integration
is easy to spot in Devices & services. Older HA versions ignore the
folder and show the generic placeholder; the integration is unaffected.
scripts/make_brand_icons.py regenerates the images with Pillow from the
test requirements; the package check verifies sizes and transparency.
Listed under 0.2.3, which has not been deployed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump the manifest to 0.2.3 for the stale-room policy: unusable room
sensors are left out with weights re-normalized, and an opt-in checkbox
keeps a stale sensor's last value for up to 24 hours. 0.2.2 was never
deployed, so the changelog marks it as such and the 0.2.3 notes point
0.2.1 users at both entries. ROADMAP and AUDIT record the decision and
the open control-design questions (minimum weight coverage, outdoor
temperature outages).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single stale, unavailable, missing or invalid room sensor paused
learning and forecasts for every room. That is not acceptable once the
MPC may control heating.
Now such a room is left out: the weighted temperature, learning and the
optimizer use the remaining rooms with their weights re-normalized, and
a room learns only across poll pairs where it was usable at both ends.
Readiness considers only the rooms used. Everything pauses only when no
room is usable or a shared input (relay, thermostat, outdoor
temperature) is missing.
A new setup/Configure checkbox, room_stale_use_last_value (default off),
keeps using a stale room sensor's last value instead, for sensors that
report only on change. It never applies to unavailable or invalid
sensors, or to a sensor silent for more than 24 hours.
Status adds excluded_rooms, weight_coverage and a per-room "used" flag.
English and Romanian labels and help text added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Add requirements-test-latest.txt pinning the newest published
pytest-homeassistant-custom-component (0.13.316, HA 2026.2.3, Python
3.13) and run the workflow as a matrix over both requirement files. No
DNS-library pin is needed for the newer release. Both environments pass
scripts/check.sh locally, with no HA deprecation warnings logged for
this integration. Remote runner execution is still unverified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing room and weather freshness limits required knowing how long
each sensor normally goes without reporting, which HA does not show.
Add max_age_seconds_24h to every source in input_quality: the largest
report age seen at polls over the last 24 hours (hourly buckets, reset
on reload, underestimates by up to one poll interval).
input_quality and weather_comparison change every poll, so the Recorder
stored a new attribute row every five minutes. Mark them unrecorded;
they remain visible on the Status entity. Model coefficients and other
attributes are still recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving wrote only the currently configured rooms, so removing a room in
Configure (even by accident) discarded its learned model at the next
save, and adding it back started from scratch.
Keep validated models of removed rooms (at most 24, oldest dropped
first) in an optional retired_learners payload key and restore them when
the room is added back. Payload schema_version stays 2: the key is
additive and older 0.2.x readers ignore it. Replacing the relay still
starts a fresh model, as before. Status lists retired_room_models.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Polling only sees the relay's current state and last change, so two or
more changes between polls (e.g. an on/off round trip) made the lag
estimate unknown and paused learning for about half an hour.
Subscribe to the relay's state changes (read-only) and integrate every
recorded change between polls. Because HA delivers these events one
loop iteration late, the current state stays authoritative and fills in
a change not yet recorded. Unknown periods (non on/off states, missed
transitions, event overflow) still restart the estimate with an
explicit error bound. Without the listener, behavior is as before.
HA skips unload callbacks after unexpected setup errors, so setup now
removes the listener itself on any failure and registers it for unload
only once setup succeeds.
Tested on HA Core 2025.12.0 and 2026.2.3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The HA suite rejects service calls at runtime, but only on exercised
paths. Parse the integration source and fail on service calls or
registration, actuator method names, service-name strings, or any
platform other than sensor/binary_sensor (AGENTS.md safety rule 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Optional weather source fields used the current selection as the form
default. A cleared field that the frontend omits was refilled with the
old sources by voluptuous, and an options key missing from the
submission fell back to the original setup data. Optional providers
could therefore not be removed in the UI.
Pre-fill with suggested values instead, and save omitted optional lists
as empty so they override older entry data. No UI strings changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A finished plan was discarded whenever any watched State object changed
during the save or calculation, including diagnostic-only weather
sensors and the thermostat's current_temperature/hvac_action. The
proposed-heat entity then went unavailable for five minutes without
cause. Compare only the values the prediction uses: room and outdoor
temperature states and units, thermostat mode and target, and relay
state and last change.
Also pass config_entry to the coordinator explicitly instead of relying
on HA's ContextVar fallback, which HA reports as deprecated usage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lag proxy was reset to the new relay command at every relay change
and after any skipped interval, so it read 1.0 straight after ON and 0.0
straight after OFF. This disabled the lag model around every boiler
cycle: on a synthetic lagged plant cycled by a hysteresis thermostat the
learner's heat gain came out 34% low (0.079 vs 0.12) and the loss term
43% low, and the optimizer started post-transition plans from the wrong
lag.
Track the lag at every poll with a valid relay state, independently of
room sensors. Integrate a single change between polls at its
last_changed time. For ambiguous cases (round trip between polls, relay
outage, restart, clock jump) restart from time-in-state with an explicit
error half-width, and learn only once it is at most 0.1 (about 32 min
after an HA restart). The same synthetic test now learns 0.1195/0.00389
(true 0.12/0.004).
Expose heat_lag, heat_lag_uncertainty and sample_result on Status so
users can see why a poll was or was not learned. The HA test fixture
relay now has an hour-old timestamp; a new test covers a freshly
written relay state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With forgetting 0.999 and no heat excitation (summer, long OFF periods),
the heat-coefficient variance grew by 1/0.999 per sample without limit:
about 8.7e12 after 104 simulated OFF days. The first ON samples could
then swing coefficients to their bounds, and accumulated asymmetry
approached the absolute 1e-8 restore tolerance, past which a restart
silently reset the whole model.
Symmetrize P and bound its diagonal by the prior after every update
(row/column scaling keeps it positive definite). Restore uses a relative
round-off tolerance and applies the same bound, so wound-up v0.2.1
models load instead of being discarded. Payload schema is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>