Commit Graph
18 Commits
Author SHA1 Message Date
buzeraandClaude Opus 5.5 2bddc74d37 Keep unknown window states from passing as closed
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>
2026-10-27 03:28:06 +02:00
buzeraandClaude Opus 5.5 e62798dc4f Test power spikes, calm wind and restart of the new model layers
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>
2026-10-27 02:57:20 +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 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 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 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 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
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