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>
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>
- 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>
During the night grid charge under the peak reserve the guard alarmed
(ERROR PEAK_GUARD_FLOOR_LIMITED, "Battery stopped limiting the peak") and the
Batteries "Why" said "The peak limit needs power" at quarter ends, while every
quarter landed on the aim. The charge lands the period 0-7 W over the aim and
excess_w()'s 900 / remaining turns that into tens of kW in the last second;
with nothing able to give, check_peak_floor_limited() alarmed on any
wanted > 0.
Each cycle the guard wants power, _peak_landing() asks misses_aim(): with the
batteries at 0 W, does the period end more than ALARM_MISS_W over the aim? If
not, the decision is within_aim: no reserve alarm, no red peak row, no "Why"
peak. The claim and the commands are unchanged. A real overrun under the
reserve alarms on the same cycle as before.
Co-Authored-By: Claude <noreply@anthropic.com>
F1: the evening rule has an age limit (KEPT_MAX_AGE_S).
F2: the evening bound is the next local midnight (_bounds), right on 25 h and 23 h days.
F5: test that old days are dropped from the stored hold.
Co-Authored-By: Claude <noreply@anthropic.com>
With battery modes off the forecast is still read and shown (Settings,
Simple, and the Forecast tab), but no plan uses it, so "used for tonight's
plan" was not true there (UX-53). "Kept for tonight" is true in every
shape. NL "vanavond" read wrong after midnight; "vannacht" covers the
whole night. DECISIONS' "modes off: nothing reads the sensors" corrected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Hold.stand_in(): day0 <= now < day0 + 3 h. A tomorrow sensor is asked with
day0 = the next midnight, so the upper bound alone was true all day and a
figure stood in at any hour. test_opt27.py covers daytime tomorrow outages.
Co-Authored-By: Claude <noreply@anthropic.com>
test_modes_p4.py: 'the forecast is lost mid-run' now loses it for good (a hold
that never covers); a second scenario is the same loss with the hold, which
keeps Leftover solar charging. test_opt27.py: a failing check no longer hides
the ones after it.
Co-Authored-By: Claude <noreply@anthropic.com>
Forecast.Solar's sensors stop updating in the evening, so at night every such
site had no forecast: the night cheap run needed 1.5x the cycle cost and
Leftover solar planned nothing. One missed poll also dropped it at once.
solarfc.Hold keeps the last fresh reading of each sensor, for the day it is
for. A missing or stale sensor is covered by it for up to FRESH_S (a short
outage), and from the end of the bell (20:00) until local midnight + 3 h
however old. A tomorrow figure is tomorrow's until midnight and then today's;
a today figure is only today's. Stored in the controller's store
(solar_fc_hold), so an evening restart keeps it. The forecast line says
'Solar forecast read 17:05, used for tonight's plan.' (EN, NL).
test_tariff24_weather.py runs with a hold that never covers: it tests what the
plan does when the forecast IS lost.
Co-Authored-By: Claude <noreply@anthropic.com>
UX-54's Forecast tab (release/1.0) re-plans tomorrow's cheap run as control
will, with the gap `required`. It still added the "no weather" x1.5 always,
while control (TARIFF-24, from master) drops it when a forecast is read. In
the UX-54 scenarios the tab asked 0.225 where control needs 0.15. It now asks
`arbitrage.weather_uncertainty(True, c.solar_fc)`, so the two agree again, as
they did on release/1.0 alone. Display only; no control change. The tab's
blocks in every UX-54 scenario, every 2 h, are identical to release/1.0.
test_ux54.py pins it (the old line fails it). The TARIFF-24 DECISIONS note
names the tab as the fourth place that reads the rule.
Co-Authored-By: Claude <noreply@anthropic.com>
- TARIFF-24 DECISIONS: the hold keeps a Cheap hours charge; a Leftover solar
battery still stops without a forecast (it has no planned level, still true
after OPT-28: `dayplan.plan()` returns None and `charge_to()` gives None).
Adds "a run that has started runs to its end" for a give run begun in a hold.
- CHANGELOG 0.19.2: "when the period is over" -> "when the period would go
over the limit".
Co-Authored-By: Claude <noreply@anthropic.com>
Part 6 of test_tariff24_weather.py, the review's probes R4 and R5:
- R4: a forecast lost while only the day's give run is under way holds to
that run's end (22:00) and no further; the night's cheap run then needs the
wider gap. Pins the `solar_fc is None` return in `_wx_track()`: without it
the hold chains into the cheap run and B charges (mutant M2, now red).
- R5: a give run that starts during a hold runs to its end (`keep_started()`),
with no grid charge after the hold. Behaviour unchanged; a give run not kept
goes red.
Co-Authored-By: Claude <noreply@anthropic.com>
With master merged in (TARIFF-24), a forecast replaces the 1.5x widening, so
the shipped cost 0.10 is now the whole gap with a forecast. test_ux54.py's
modes-on scenarios were written for 0.15 and 21 of its checks failed on
release/1.0 + master (they pass on release/1.0 alone). They now set the cost
to 0.15 where they have a forecast: the same gate as before. Scenarios with no
forecast (nofc, stale) and modes off keep 0.10, widened as before.
(cherry picked from commit 665653517c, branch OPT-27)
Co-Authored-By: Claude <noreply@anthropic.com>
Brings in 0.19.1 (TARIFF-24) and 0.19.2 (SAFETY-125). Git merged every file
without a text conflict. config.yaml's version becomes 0.19.2, the newest
CHANGELOG section: test_version_gate.py (PKG-14, 11b) requires the two to be
equal, and release/1.0 took 0.19.0 the same way (#207). The 0.19.1 and 0.19.2
CHANGELOG sections come in as history.
Co-Authored-By: Claude <noreply@anthropic.com>
With A on Leftover solar and B on Cheap hours (the dynamic scenario, and the
owner's planned setup), the line said "The batteries cannot hold it all. The
grid covers N kWh", while B holds a full charge for the same hours. It now
says "The expensive hours need more than Leftover solar can hold. The rest,
N kWh, comes from another battery or the grid." (NL likewise). DOCS,
DECISIONS and the dayplan/homeview docstrings say the same. Text only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
OPT-23: a state-only forecast is read from now for every span
(Forecast.between since=), so the plan's per-hour sun adds up to the day's
figure, today and tomorrow. Hourly splits are unchanged. The Forecast tab
reads spans the same way.
OPT-28: a Leftover solar battery buys, from the floor, the sum over the
expensive hours of max(0, house - trust x sun), / EFF. Several Leftover
solar batteries hold it together by capacity, the ceiling passes the rest
on, and what none can hold is said (Plan.uncovered_kwh). Home's grid block,
the Batteries line and the mode help (EN + NL) say the level, what it
covers, and that Leftover solar buys only what the sun will not give.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Owner 2026-10-29: fix N=1 in this hotfix. _charge_headroom() no longer
returns early at one inverter, so the N=1 cheap-window maintenance charge
is held to charge_room_w() as at N=2; SAFETY-84's site-stop rule stays N>=2
only (at N=1 such a stop outranks the shaping anyway). The guard no longer
steps in and gives power at each quarter's end during that charge.
test_safety125.py: N=1 maintenance on 5 s and 10 s meters with late
readings, with and without a banked peak; the row in an N=2 maintenance
charge and in shadow mode; edge cases (blind past the hold, a start
mid-quarter, the summer-time change, the forecast in W, a 10 s meter, the
last second). test_safety84/87: three N=1 pins move. test_safety119: its
quarter-edge hand-back runs keep the old N=1 sizing as a fixture, plus a
check that with the new sizing nothing hands back. CHANGELOG, DECISIONS,
DOCS follow.
Co-Authored-By: Claude <noreply@anthropic.com>
- The over-the-limit rows ran over STYLE's 15 words (EN 16/19, NL 16/21)
and two Dutch Home sentences over 12 words; they were not in the samples,
so test_messages/test_ux46 never saw them. Now:
"Peak guard: grid charge limited, at this rate 5 814 W, over 5 413 W" /
"Piekbewaking: laden beperkt, aan dit tempo 5 814 W, boven 5 413 W",
"...idle, at this rate ..., over ..." / "...in rust, aan dit tempo ...",
NL Home "Laadt minder van het net om onder 5,4 kW te blijven." and
"Zou minder van het net laden om onder 5,4 kW te blijven."
The rows and the line are added to the samples. The under row is unchanged.
- CHANGELOG known issues and the DECISIONS note: at N=1 the maintenance
charge is not sized to the room, so SAFETY-99 still applies to it.
- DOCS: the peak guard row's new words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A meter reading late near a quarter's end left seconds the projection did
not count (SAFETY-99), and charge_headroom_w() read them as room: the cheap
run (and price trading, and the N=2 maintenance charge) went back to full
power in the quarter's last seconds and the quarter could land over a banked
month peak or the cap. peakguard.charge_room_w() counts those seconds at the
held reading for the charge only; the guard's floor is unchanged.
The guard row said "idle, heading for 5 814 W, under this month's 5 413 W
peak". Over the limit at the current rate the row and the month-peak line
now say so, and that the grid charge is cut back.
Version 0.19.2, CHANGELOG, DECISIONS. test_safety125.py new; the lag pins
in test_price_run_fixes.py and test_opt20.py and the AST pin in
test_arbitrage.py follow the change (see the pull request).
Co-Authored-By: Claude <noreply@anthropic.com>
Losing the solar forecast mid-run used to widen the gap at once and stop a
grid charge (and a sensor that went unavailable for a minute stopped and
restarted it). A run under way when the forecast is lost now keeps the gap it
was planned with until it ends (Controller.weather_held(), recorded by
_wx_track() each cycle a forecast is read); the wider gap applies from the next
run. The plan key holds the hold state so the plan rebuilds when it ends. Home's
mirror (homeview.trade_verdict) asks the same weather_held(now).
Tests: test_tariff24_weather.py part 4 (going: the charge continues to the run's
end, the next run is not_worth; flap of one and two reads: no stop, no restart;
Home agrees) and a test_modes_p4.py scenario that runs a forecast day at a gap
between 1x and 1.5x the cycle cost. DECISIONS wording follows.
Co-Authored-By: Claude <noreply@anthropic.com>