Commit Graph
26 Commits
Author SHA1 Message Date
buzeraandClaude Opus 5.5 17dbe76223 Release 0.4.0
Tests / tests (requirements-test-latest.txt) (push) Successful in 1m45s
Tests / tests (requirements-test.txt) (push) Successful in 1m42s
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>
2026-10-27 03:02:21 +02:00
buzeraandClaude Opus 5.5 dc92e2bac5 Keep the entry loaded when the last zone is deleted
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>
2026-10-27 03:02:03 +02:00
buzeraandClaude Opus 5.5 b1577a0c78 Score the full model against the base model; compile the search step
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>
2026-10-27 02:55:57 +02:00
buzeraandClaude Opus 5.5 8d197fabcd Explain each zone: contributions, forecast steps, coefficients
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>
2026-10-27 02:50:56 +02:00
buzeraandClaude Opus 5.5 ed8f9fe2e2 Model windows, ventilation, wind exposure and recovery
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>
2026-10-27 02:47:40 +02:00
buzeraandClaude Opus 5.5 00e70c612b Model electrical heat sources (UPS, gaming PC) per zone
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>
2026-10-27 02:39:18 +02:00
buzeraandClaude Opus 5.5 46587af8a3 Introduce thermal zones as config subentries
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>
2026-10-27 02:32:11 +02:00
buzeraandClaude Opus 5.5 220ac570d2 Add a generic named-feature learner for the zone models
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>
2026-10-27 02:25:03 +02:00
buzeraandClaude Opus 5.5 e59c08f586 Release 0.3.0
Tests / tests (requirements-test-latest.txt) (push) Successful in 1m30s
Tests / tests (requirements-test.txt) (push) Successful in 1m39s
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>
2026-10-26 22:39:25 +02:00
buzeraandClaude Opus 5.5 d2bd398a0f Show learning activity with Learning Progress and Last Sample sensors
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>
2026-10-26 22:39:25 +02:00
buzeraandClaude Opus 5.5 3afb6688a5 Score forecasts against reality; expose forecast trajectories
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>
2026-10-26 22:35:13 +02:00
buzeraandClaude Opus 5.5 43915ba810 Count rejected samples and poll outcomes; pin rollback contract
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>
2026-10-26 22:30:16 +02:00
buzeraandClaude Opus 5.5 ec3a0738fe Add integration icon
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>
2026-10-26 22:13:20 +02:00
buzeraandClaude Opus 5.5 c79fdbd71b Release 0.2.3
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>
2026-10-26 22:07:52 +02:00
buzeraandClaude Opus 5.5 3076beebfb Leave out unusable rooms instead of pausing; add keep-last-value option
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>
2026-10-26 22:07:18 +02:00
buzeraandClaude Opus 5.5 cd8b65ac2f Release 0.2.2
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>
2026-10-26 21:55:02 +02:00
buzeraandClaude Opus 5.5 4daba5028b Show each sensor's longest report gap; stop recording volatile attributes
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>
2026-10-26 21:51:35 +02:00
buzeraandClaude Opus 5.5 c0e4de832b Keep models of rooms removed in Options
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>
2026-10-26 21:50:36 +02:00
buzeraandClaude Opus 5.5 955c679f5e Timestamp relay changes with a read-only state listener
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>
2026-10-26 21:49:34 +02:00
buzeraandClaude Opus 5.5 30f78fb6a9 Allow clearing optional weather sources in Options
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>
2026-10-26 21:21:38 +02:00
buzeraandClaude Opus 5.5 2a0a94c640 Discard predictions only when their inputs change
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>
2026-10-26 21:21:25 +02:00
buzeraandClaude Opus 5.5 c3646ba876 Track radiator lag across relay changes
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>
2026-10-26 21:21:14 +02:00
buzeraandClaude Opus 5.5 387929b01b Bound learner covariance to stop RLS windup
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>
2026-10-26 21:20:50 +02:00
buzera cea79b6b48 Add sensor freshness checks 2026-10-26 18:13:00 +02:00
buzera c434f3995d Fix shadow observer lifecycle, learning and prediction correctness 2026-10-26 17:50:20 +02:00
buzera 2b9a10979b initial commit v0.2.1 2026-10-26 17:34:32 +02:00