The cheap run is sized at the full rating, so under the peak limit a battery is
still short when it ends, and control plans the next run then. Home drew only
the run under way. `cheaproll` rolls the run on the way control does
(`modetrade.energy()` + `priceruns.plan()` at each run end, lifted from
`dayview._levels()`), reading the levels at the charge power the peak limit
leaves (peak aim less the house, else the rating). `homeview.mode_runs()` and
`price_windows()` use it for today: the grid block, the Today line and the
chart's cheap bars run to where control stops, each battery's block to its
own stop. Display only: no control change.
test_ux54: today's grid block of the night-before scenario now follows the roll.
Co-Authored-By: Claude <noreply@anthropic.com>
Brings OPT-36 (#229) and TEST-37 (#231) onto release/1.0, which keeps
SAFETY-128 (#228). No textual conflicts. CHANGELOG: the SAFETY-128 entry
moved out of the released 0.20.0 section into "Not in a release yet" above
## 0.20.1; the 0.20.0 and 0.20.1 sections are identical to master.
Co-Authored-By: Claude <noreply@anthropic.com>
The stale_live scenario stores a latch ending 2026-11-04 07:15 CET. The first
Controller was built before the datetime patch, so Controller.__init__ judged
that latch against the real clock and it expired on 2026-11-04. Build it after
the patch (clock = scenario start), as the restart path already did. Test-only.
Co-Authored-By: Claude <noreply@anthropic.com>
SAFETY-132: the 9.6 kW is the charge the add-on asked for in a test,
not a measured grid figure. SAFETY-133: the quarter also ends over the
3 500 W limit; the guard sees the load late (0.20.1 has no lateness check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patch bump: OPT-36 (Leftover solar counts Cheap hours only and Solar first batteries). No option changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Review F1: a slot after B's run counts only if no slot from the expensive
hours on is cheaper (control plans B's charge left in the cheapest window
ahead), and x 0.95 of B's charge is stored. Three-price night: 1ac7fe9 left
the expensive hours 2.61 kWh short; now 0.
Review F2: a Solar first battery's drain until the expensive hours reads each
hour at no less than the night base (the profile is net of the sun). Read net,
a dark morning drained the battery the plan still counted; A's level fell,
the cheap run moved and was lost before it started (+0.40 EUR per 3 days).
The two real-price replays now cost what 0.20.0 does.
F3: the ux54 sweep's peak limit is 15 kW (the option's top). F4: the rest
comes from the grid (EN/NL). F5: CHANGELOG and DECISIONS wording.
Co-Authored-By: Claude <noreply@anthropic.com>
The run is sized at the batteries' ratings, not the peak limit, so a run too
short to fill a Cheap hours battery is planned again later in the night.
Counting that run alone made B's give grow through the night and A's level
fall after A had bought (round 2: 55 -> 69 -> 41 -> 37 %, A to 56 %).
dayplan._cheap_give_kwh() now also counts the later firm slots before the
expensive hours that a run would still be worth charging in (price + the
spread control asks at most the give run's dearest slot); the Controller
passes the slots and the spread.
test_opt34: back to the original house shapes, fixture and expected values.
B is Off in the nights that test A's level (a Cheap hours B now covers
these hours); B's checks run in new `_b` nights; the 03:00 check runs in a
night from midnight; tail starts A at 60 %; late_fresh reads the kept 6.0.
test_opt36: slot cases, a whole night on one figure (level never moves),
F-1 with B counted (raised level reached and held). test_ux54: the run end
is read from the plan and asserted (02:30); the F1/F2 sweep uses a peak
limit that does not bind and reads a level shown as the target as at it.
Co-Authored-By: Claude <noreply@anthropic.com>
The silent and fresh nights diverged only because the test's fresh sensors
kept reading the evening's 0.4 kWh 'left today' after midnight, while the
kept figure is the 4th's 6.0 kWh (solarfc.Hold.stand_in, as_today): two
different shortfalls (16.5 and 12.7 kWh) that A's ceiling hid until B was
counted. Fresh sensors now read 6.0 after midnight, so fresh and kept are
the same figure; the 0.3 tolerance is back, the other exact values are
recomputed. test_ux54 literals are recomputed too.
Co-Authored-By: Claude <noreply@anthropic.com>
Review B1: the meter fit took repeating house loads (ovens, a heat pump)
for late readings on healthy 1-2 s sites, about 9 % of the recorded
house. B2: at a 5 s meter it never ran. M1: no hysteresis. M2: an
unbounded check.
lateness.Delay now fits how long each battery's reading takes to show
its own request, which the house does not move, from the 1 s reads
(read_inputs -> Answer.watch). The meter fit stays for speed at 1-3 s
meters, but counts only when an ask was slow to start and no battery
fit said on time. Late from 9.5 s after 2 fits, on time under 6.5 s,
dropped after 120 s without a fit. Fits are bounded (a few ms).
Readings on time: no verdict in any reviewer scenario (22 shapes at
1/2/5 s meters, 7 harm runs) nor in 2 days of the recorded house at
1, 2 and 5 s ticks; digests equal to 77fe31b. Late: found 36-68 s after
the start at 1-2 s, about 3 min at 5 s. The row gives the batteries'
delay, or no figure while only the meter has shown it.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
With Cheap hours B counted, A buys only what B cannot reach. The OPT-34
night scenarios use a 1.6 x larger house profile, and the ux54 tab scenarios
a 4.8 kW evening (ux54_scenarios.make(evening=)), so A still charges; the
hard-coded levels become relations.
Co-Authored-By: Claude <noreply@anthropic.com>
Live 0.20.0: A (Leftover solar, 60 %) charged to 95 % while B (Cheap hours,
94 %) already covered the expensive hours. dayplan.plan() now takes off the
shortfall what Cheap hours batteries will hold when the hours start (the
ceiling or what their cheap run reaches under the peak aim) and what Solar
first batteries will still hold after the night's use. Home reads a give
block after the planned charge, plan_cover() is the plan's own figure, and
the Why line says other batteries cover.
Co-Authored-By: Claude <noreply@anthropic.com>
Readings that Home Assistant delivers late but still changing passed
every staleness gate, so the loop corrected again each cycle an error
its last command had already fixed (10 s late: the give run swung and
exported to -1.9 kW), and the peak guard pushed to the inverter limit
on the same late figures (OPT-34 F-2).
app/lateness.py fits how long after a power request the meter shows
it. Over 8 s the readings are late: a request the battery reading has
not answered holds the loop (it may not lead the reading by more than
100 W) and caps the peak guard at what stands; unanswered for 23 s it
holds, then 0 W after stale_input_s. An amber row and one log line per
episode say so. With readings on time nothing changes (digests equal).
DECISIONS/SAFETY-128-late-readings.md; test_safety128.py.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
- The OPT-23 headline said a daily-total forecast is now counted right by
day; OPT-30 (known issue) still over-counts by day. Say what changed.
- CI-15: 16 min was one run; runs took 16 to 24 min.
- UX-75: quote the page text as it reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a fresh today reading in the night (02:00 on the rig) went stale
(05:00), Hold.stand_in(as_today=True) served the 17:05 "tomorrow" figure
again until 06:00: the plan went back to 95 %, the latch took it, and
Leftover solar charged back up. read_sum() now skips that stand-in once
every today sensor has given a fresh reading for the day
(Hold.replaced()); the stale newer reading follows the OPT-27 rules.
Co-Authored-By: Claude <noreply@anthropic.com>
Comment, docstring and DECISIONS only (same-code script OK; python -OO
bytecode now identical for all six files).
- peakguard.charge_room_w: one-line docstring + SAFETY-125 pointer (cut whole).
- peakguard.shaping_suspended: keep 'the one place this conjunction is
written; do not inline it'.
- price_forecast module note: an hour with no price and no yesterday is left
out, never filled (test_price.py 14d cites the module note for it).
- Refill comment blocks the cut left ragged; DECISIONS pointers on one line.
- FCST-04 note: _yesterday_of(), not _previous_day(); FCST-03 note: the 49
stronger cases are why the rule is a real change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
- F1: an expired latch is dropped whatever the run state; plan_targets() and the
store loader ignore one that has ended (no stale latch after a restart).
- F2: the "charge goes on" line only while a cheap run is on.
- F3: a battery that is off or no longer Leftover solar leaves the latch.
- F4: a run carries the latch on only if it starts by the bell start (06:00
local) after the latch began (manager's call, 2026-10-31).
Tests: test_opt34 scenarios stale/stale_live and part 5; test_modes_p4 asks at a
moment inside the latch.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
test_opt34.py part 4 drives Controller._latch_charge() directly: a battery
that leaves the plan is dropped at once, does not rejoin while nothing
charges, an empty latch is cleared from the store, and the screen says the
charge goes on only while the latched battery is below its level. Catches
two reviewer mutants that survived the round-2 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a forecast is present and the cheap run holds, the night-charge latch is
rebuilt from the plan: a latched battery takes its current level whatever its
role this tick (a fresh figure may lower it below the SoC), one that left the
plan is dropped, a new one comes in only while it charges from the grid.
Test: a fresh brighter figure lowers A below its SoC, the sensors go quiet, A
does not charge back up. CHANGELOG summary says two fixes.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
With the forecast sensors silent from 17:05 the kept evening figure ended at
midnight + 3 h, in the middle of the night grid charge, and Leftover solar's
planned level went with it (rig: A 49.8 % against 79.9 %).
- A night grid charge that has started keeps its planned level when the forecast
goes (Controller._latch_charge, store key night_charge_latch), and the narrow
price gap with it (weather_held). A fresh figure still moves the level.
- The evening's tomorrow figure is today's until the bell starts (06:00 local),
up to 15 h old but only if it was usable at midnight (solarfc.Hold.stand_in).
- Screen: "No solar forecast now. The charge under way goes on to its planned
level." (EN/NL) while a latched battery is below its level.
- test_opt34.py (whole nights through the real control cycle, mutants), test_opt27
and test_modes_p4 adjusted to the new window and the kept level.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Text only (DECISIONS, DOCS, CHANGELOG): a step load alarms on the same cycle
as before; a steadily rising load alarms a little later; with peak_margin_w 0
a period can end up to 10 W over the limit without this alarm, as for 'Load
too high' (SAFETY-81). Owner accepted 2026-10-31.
test_safety126.py part 2: after a real overrun ends, _peak_landing()'s latch
is reset and the night's later landings are quiet (review F3).
Co-Authored-By: Claude <noreply@anthropic.com>