A meter gap past meter_max_age_s left seconds the quarter average never
counted, so the guard was blind for the rest of the quarter (rig case 7b:
3964.8 W against a 3450 W aim). Once readings are back, the missed seconds
now count at the higher of the limit and the import seen just before and
just after the gap with the batteries at 0 W (peakguard.gap_w/fill_gaps);
the gap's first meter_max_age_s is topped up to that too. Seconds before
the add-on's first reading count at the aim, and the page says part of the
period was not measured (EN/NL). The bill record is unchanged; no option.
DECISIONS/SAFETY-90-peak-guard-after-a-gap.md, test_safety90.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: modetrade.decide() takes both the day plan (targets) and
SAFETY-78's draining flag; the Why line keeps SAFETY-78's other_cycle
ahead of the plan's word (test added).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- DECISIONS: the Cheap hours change and why; the no-sun replay (the gain is
Leftover solar buying the evening's shortfall; the forecast added 0.14
EUR); the reconstruction bias stated plainly.
- DOCS: Cheap hours ignores the forecast; a reading from before midnight
is stale.
- The forecast line: "Solar still to come today: ..." (it is what is
left, and it stays within the 12-word limit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cheap hours only fills to the ceiling with or without a forecast, as the
design (section 1) says. Skipping the sun's share cost 0.31 EUR on real
2026 prices (29.72 against P3's 29.41) and saved nothing on the P3 set.
- A forecast reading from before local midnight is stale: until the
integration refreshes, "left today" is still yesterday's 0 and
"tomorrow" is really today.
- Forecast entity ids must be sensor.[a-z0-9_]+ (they go into the
/api/states/<id> path).
- House profile: a stored day that is not a date, or is after today, is
dropped instead of kept forever.
- Text: the forecast line says "for the rest of today"; the learning line
no longer promises "tomorrow" (a plan needs one full day).
- Tests for four surviving review mutants (hour sample floor, the dear run
after the cheap run, share by room, daytime use) and the fixes above.
- Replay tool: `--fc zero` (a forecast of no sun).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At two inverters the drain pins its pack and the site target is split
around the pin. Below the pin the other pack had to charge, the allocator
refused the opposite directions, and the site sat at 0 W with an ARBITER
line every cycle (house below or above the drain rate, or a grid charge).
main.py now files a site limit `drain` (lo = what the pin places, at
P_MAINTENANCE) at N>=2 while a pack drains, so the other pack only adds
power in the same direction. A drain that cannot give power waits with
one log line. With battery modes on, no grid charge while a pack drains,
and the other battery's card says why. N=1 is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Home Assistant solar forecast (Forecast.Solar, Solcast or Open-Meteo
shapes) and the add-on's own hourly house use decide, before each cheap
run, whether the sun will fill each battery before the expensive hours.
Leftover solar then buys from the grid only the shortfall; Cheap hours only
skips the part the sun covers; Solar first never grid-charges. No, stale or
failed forecast: P3 option A exactly (12 digests pinned from 5a4cdc2).
Replay (16 recorded days, forecast errors from Elia's day-ahead BE solar):
year-earlier prices P4 19.15 vs P2 19.33 / option A 19.44 / old trade 18.14;
real 2026 prices P4 29.19 vs 32.26 / 32.01 / 31.40 EUR net.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Step 5 said "Choosing a mode for each battery comes in a later update",
stale since P1. It now points to battery modes on the Batteries screen
(EN/NL); test_setup checks the new line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision 2026-09-29. Selling is off until it can be judged on the price
the grid pays: packmode.SELLING_ENABLED = False makes read() answer
sell=False whatever the store holds, pack_sell is refused ("Selling is off
for now."), and the page has no selling switch; the sell code stays. Until
the P4 forecast, only Cheap hours only charges from the grid
(packmode.GRID_CHARGE_MODES); Leftover solar holds and gives in the
expensive runs. Help, Today and plan lines, DOCS and DECISIONS follow.
Replay, 16 days: Solar first + Leftover solar 19.44 EUR net (P2 19.33).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A sale block on the plan no longer says "Worth up to" at the import
price: a sale earns the lower injection price.
- The selling switch help (EN/NL) and DOCS say the grid pays less than
power costs; DECISIONS records why P3 loses on the replay.
- test_modes_p3 part 11: modes on ignore the site trade switch (R13), a
battery in maintenance takes no part (R10), a headroom stop latches for
the period (R8). All three mutants survived before and are red now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cheap and expensive hours as runs of consecutive firm price slots
(priceruns.py); each battery's role per tick from its mode (modetrade.py):
grid charging for Leftover solar / Cheap hours only, never Solar first;
holding for the expensive runs; selling per battery (pack_sell) above the
spread and the floor. The site trade is sized inside the modes (loop bound)
and the peak guard's headroom; a run edge reseeds the loop. Cheap hours only
and selling are enabled on the Batteries screen and in the HA select
(appended). The site Why row no longer says "held back" when the peak limit
pushes power up (F2). Modes off: byte-identical to 7a00f12 on the trade path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The peak limit's Why and plan block need a real power request (floor above
0 W), not a blind or floor-limited guard. An Off battery gets the peak
block only while it gives power. Why no longer says every battery gives.
- test_batteries_screen pins the fixture clock: unpinned it failed every
morning. New checks cover the trade and cycle filters, the blind guard and
an Off battery that A covers alone.
- Settings keeps only a picked view; a link to Installer is not remembered.
- Browser check: badges, unsaved change across views, locked value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New option peak_margin_w (default 50 W, 0-500 W). The peak guard, the
forecast checks and the maintenance charge sizing aim at
peak_cap_w - peak_margin_w; the month peak, the bill, headroom, warnings
and every notice keep the real cap. Owner decision 2026-09-28.
peak_margin_w 0 is byte-identical to ed739d4 (tools/safety79_identity.py,
test_peak_margin.py part 1). validate.py rule 12 refuses a value outside
0-500, not whole, or not below the peak limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
- Batteries: one card per battery with On/Off, the mode list (cheap hours
and selling greyed out until OPT-11 P3), level, and Now / Today / Why
lines built in homeview.py. Replaces the OPT-11 P1 minimal cards and uses
the existing pack_use / pack_mode actions and store keys.
- An Off battery the peak limit still needs says so with its figure; no line
shows 0 W beside moving power (OPT-11 P1 test F2). Today says "Not planned
yet" until OPT-11 P3/P4.
- Home's plan leaves Off batteries out, except for the peak limit.
- Settings opens on Simple: the layout's simple fields with an applies chip
and a note, plus the battery switches and modes. Installer is one tap.
- English and Dutch words in app/ui_text; test_batteries_screen.py; browser
check tools/ux13_browser_check.py (CHECK=1 ux13_screenshots.sh); new and
changed screenshots only.
No control, allocator, arbiter or command module changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
A battery's mode is a soft unit limit (source `mode`, P_MODE 35) kept
apart from the hard limits in pre_resolve(). The allocator applies it
only while the site target is decided at or below 35 (new
Resolution.deciding_priority), so the peak floor releases it and a mode
`hi` never meets the site `lo`. At N=1 resolve() applies the same rule
to the site target after stage 2. A mode range without 0 W is dropped
as a software fault, so REFUSE_SIGN stays unreachable from a mode.
compute() gets the loop bound (sum over live units of hard limits with
modes applied), so the loop never asks for watts no battery may give
and the saturation freeze never parks on them. While charging, Leftover
solar weighs 0 so Solar first fills first. Store keys pack_use and
pack_mode per battery, set by two new /action values; Off stops and
skips maintenance with a log line. Minimal Batteries cards (EN/NL).
Off by default (battery_modes_enabled); off, commands are byte-identical
to a5dddad at N=1 and N=2 (test_modes.py part 1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_ui_text.py: key parity, placeholder parity, English-left-in-Dutch,
semicolon, 25-word description and 2-sentence line each now fail on a
spoiled input (the English-left check was not a real mutation).
- Jargon lists now cover the whole 'Not' column of STYLE.md section 4, in
English, with the Dutch equivalents.
- nl.json: 'gewijzigd worden' where 'wijzigen' read as intransitive,
'Bezig met bewaren' for the saving state, 'Metingen kloppen niet'.
- test_uimodel.py: an overlap that changes partners is named piece by piece
(a surviving mutant of _tiling's partner check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- app/ui_text/nl.json and app/uitext.py: every page word in Dutch, English
fallback per key; the page picks by browser language. Home's sentences
move into ui_text `home_words`, so /status?lang=nl builds them in Dutch.
- test_ui_text.py: EN/NL key and placeholder parity, STYLE limits, jargon
lists for both languages, no inline details, untranslated Dutch caught.
Shared rules moved unchanged to tools/style_rules.py.
- tools/ux13_screenshots.sh/.py: DOCS screenshots from the demo fixtures in
headless Chromium; docs/img/ux13/. Battery modes (P3): no screenshot.
- "Other time windows": a list editor (ui.periods) that builds
static_profile and names bad times, empty prices, overlaps and gaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>