Commit Graph
689 Commits
Author SHA1 Message Date
admin deee98d352 Merge pull request 'SAFETY-126: no false peak reserve alarm during the night grid charge' (#221) from SAFETY-126 into release/1.0
infra-pin / fresh (push) Successful in 2s
2026-10-31 12:26:03 +01:00
glenn schrooyenandClaude a66a7f1a0f SAFETY-126 round 2: state what the 10 W tolerance costs; test the latch reset
checks / unit (pull_request) Successful in 18m11s
infra-pin / fresh (pull_request) Successful in 2s
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>
2026-10-31 11:52:27 +01:00
glenn schrooyenandClaude 8e2463bc32 SAFETY-126: test reads through named_absence; points at MUTATION.md
checks / unit (pull_request) Successful in 19m25s
infra-pin / fresh (pull_request) Successful in 1s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-31 01:54:16 +01:00
glenn schrooyenandClaude 40595e42ae SAFETY-126: a landing error on the aim is not an overrun
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>
2026-10-31 01:26:02 +01:00
admin cc9bbeb73e Merge pull request 'OPT-27: the night cheap run is judged with the last fresh forecast' (#219) from OPT-27 into release/1.0
infra-pin / fresh (push) Successful in 2s
2026-10-30 22:16:32 +01:00
glenn schrooyenandClaude b9dea6d90d OPT-27 round 3: the kept-forecast wording states the 12 h limit (text only)
checks / unit (pull_request) Successful in 15m27s
infra-pin / fresh (pull_request) Successful in 1s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-30 21:29:29 +01:00
glenn schrooyenandClaude 13b61d586a OPT-27 round 3: a kept figure is at most 12 h old; DST-safe midnight bound; pruning test
checks / unit (pull_request) Successful in 15m37s
infra-pin / fresh (pull_request) Successful in 1s
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>
2026-10-30 20:41:34 +01:00
glenn schrooyenandClaude Opus 5.5 307cb47caf OPT-27 review: the kept forecast line says "kept for tonight"
checks / unit (pull_request) Successful in 17m20s
infra-pin / fresh (pull_request) Successful in 1s
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
2026-10-30 19:59:29 +01:00
glenn schrooyenandClaude 60aa7f310c OPT-27: a kept figure sizes the night under OPT-28's shortfall, as a fresh one does
checks / unit (pull_request) Successful in 17m22s
infra-pin / fresh (pull_request) Successful in 1s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-30 18:57:37 +01:00
glenn schrooyenandClaude bf842997c1 OPT-27 round 2: a kept tomorrow figure is bounded on both sides
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>
2026-10-30 18:54:41 +01:00
glenn schrooyenandClaude 4fde0b177c OPT-27: test_modes_p4 says what a covered sensor does; test_opt27 guards
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>
2026-10-30 18:54:41 +01:00
glenn schrooyenandClaude 50b567d6d1 OPT-27: the night plan keeps the last fresh solar forecast
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>
2026-10-30 18:54:41 +01:00
admin f1b72443ff Merge pull request 'Sync 0.19.x into release/1.0: TARIFF-24 and SAFETY-125, with the forward-port fixes' (#220) from sync-0.19 into release/1.0
infra-pin / fresh (push) Successful in 2s
2026-10-30 18:51:53 +01:00
glenn schrooyenandClaude 60a3fa615e TARIFF-24 forward-port: part 6 reads its last row through nth() (test_named_absence)
checks / unit (pull_request) Successful in 28m5s
infra-pin / fresh (pull_request) Successful in 2s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-30 08:30:25 +01:00
glenn schrooyenandClaude 1c9fefd9dd Sync 0.19: the Forecast tab extends a cheap run with control's gap (TARIFF-24)
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>
2026-10-30 08:11:42 +01:00
glenn schrooyenandClaude 8cca671a05 TARIFF-24 / SAFETY-125 forward-port: record text (review F2, F3, CHANGELOG nit)
- 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>
2026-10-30 08:09:44 +01:00
glenn schrooyenandClaude 9dc22fb987 TARIFF-24 forward-port: pin the hold's edges (review F1, F2)
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>
2026-10-30 08:09:44 +01:00
glenn schrooyenandClaude 5ac1017eed UX-54 scenarios keep the gate they were written for (cycle cost 0.15 with a forecast)
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>
2026-10-30 08:09:44 +01:00
glenn schrooyenandClaude 8bf54ece3d Merge master (0.19.2, f1c9733) into sync-0.19: TARIFF-24 and SAFETY-125 reach release/1.0
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>
2026-10-30 08:09:44 +01:00
admin f1c9733c32 Merge pull request 'Release 0.19.2: SAFETY-125 grid charge stays under the month peak' (#217) from hotfix-0.19.2 into master
Merge hotfix-0.19.2 (SAFETY-125): release 0.19.2
v0.19.2
2026-10-30 07:44:25 +01:00
admin b6be31286a Merge pull request 'OPT-23, OPT-28: the day plan counts the sun right, and the night charge covers the expensive-hour shortfall' (#218) from OPT-23-28 into release/1.0
infra-pin / fresh (push) Successful in 1s
2026-10-30 02:55:55 +01:00
admin 6be7e41de1 Merge pull request 'CI-15: run the add-on's test files in parallel' (#215) from CI-15 into release/1.0
infra-pin / fresh (push) Successful in 1s
2026-10-30 00:29:18 +01:00
glenn schrooyenandClaude Opus 5.5 ae0cd542c0 OPT-23-28 review: the unheld-shortfall line says another battery or the grid
checks / unit (pull_request) Successful in 50m6s
infra-pin / fresh (pull_request) Successful in 1s
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
2026-10-29 23:21:31 +01:00
Claude a645d899f4 CI-15: test_safety87.py joins the serial group (real-clock meter freshness)
checks / unit (pull_request) Successful in 16m3s
infra-pin / fresh (pull_request) Successful in 2s
Its status page says "Meter: no reading for N seconds, limit 30 seconds" when
N passes 30; N is about half the file's run time. It failed alone under
--cpus 0.15 (94 s), in a loaded local parallel run (103 s) and in a loaded CI
run (112 s, run 1434). Idle it takes ~25 s. No test file changed, so it runs
alone after the parallel batch, like test_loop.py and test_freeze.py.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-29 21:52:58 +01:00
glenn schrooyenandClaude 995028bc5d OPT-23, OPT-28: the day plan counts the sun right, and the night charge covers the expensive-hour shortfall
checks / unit (pull_request) Successful in 55m9s
infra-pin / fresh (pull_request) Successful in 1s
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
2026-10-29 21:02:18 +01:00
Claude ffb0d61d5b CI-15: run the add-on's test files six at a time, the wall-clock ones alone after
checks / unit (pull_request) Successful in 19m52s
infra-pin / fresh (pull_request) Successful in 3s
The serial loop summed 3 557 s on SAFETY-125's run and passed the runner's
60-minute cap. The loop moves to .gitea/ci/run-tests.sh: xargs -P 6, longest
files first, then test_loop.py and test_freeze.py (TEST-30 / CI-06) alone on an
idle machine. Same PASS/FAIL lines, CI: echo, tail -40, :ro mount,
PYTHONDONTWRITEBYTECODE=1, GOODWE_REQUIRE_HWSIM=1 and NO test_*.py FOUND guard;
one output file per test file; any failing or unreported file fails the job.
timeout-minutes 70 -> 40, comment corrected. No test file changed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-29 20:39:20 +01:00
admin 8966867968 Merge pull request 'SAFETY-125: a grid charge stays under the peak aim (hotfix 0.19.2)' (#214) from SAFETY-125 into hotfix-0.19.2
checks / unit (pull_request) Successful in 1h3m7s
infra-pin / fresh (pull_request) Successful in 2s
2026-10-29 18:21:15 +01:00
glenn schrooyenandClaude 2929ece7da SAFETY-125 round 2: the one-inverter maintenance charge is sized to the room too
infra-pin / fresh (pull_request) Successful in 1s
checks / unit (pull_request) Successful in 1h0m42s
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>
2026-10-29 14:57:39 +01:00
Claude (SAFETY-125 review) 6eebd4882b SAFETY-125 review: guard row and Home line within UX-08 limits; N=1 maintenance named
checks / unit (pull_request) Successful in 56m45s
infra-pin / fresh (pull_request) Successful in 1s
- 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>
2026-10-29 12:56:01 +01:00
glenn schrooyenandClaude 88ab8a50e3 SAFETY-125: a grid charge counts a late meter reading's seconds; the guard row says "over" when it is
checks / unit (pull_request) Successful in 56m44s
infra-pin / fresh (pull_request) Successful in 1s
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>
2026-10-29 02:08:38 +01:00
admin 2932f3af39 Merge pull request 'Deploy 0.19.1' (#213) from hotfix-0.19.1 into master
Reviewed-on: #213
v0.19.1
2026-10-28 23:29:06 +01:00
adminandClaude Opus 5.5 7e77c723b9 Merge pull request 'TARIFF-24: battery modes start the tariff task; a forecast replaces the 1.5x widening' (#212) from TARIFF-24 into hotfix-0.19.1
checks / unit (pull_request) Successful in 51m58s
infra-pin / fresh (pull_request) Successful in 2s
Reviewed head 9768631 (Opus review). CI run 1422 green on that sha.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:14:44 +01:00
glenn schrooyenandClaude 97686319f3 TARIFF-24 round 3: a run planned with a forecast keeps its gap to its end
checks / unit (pull_request) Successful in 51m57s
infra-pin / fresh (pull_request) Successful in 1s
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>
2026-10-28 17:17:56 +01:00
glenn schrooyenandClaude d424b862d8 TARIFF-24 round 2: test_modes_p4 scenarios keep the 0.30 gate they were written for
checks / unit (pull_request) Successful in 49m18s
infra-pin / fresh (pull_request) Successful in 1s
With a forecast read the gap needed is the cycle cost (0.20), not 1.5 x (0.30),
so a midday 0.21 gap now plans a grid top-up that four scenarios (sunny, Cheap
hours partly, forecast too high, refusal) are not about. They ask for cost 0.30
with the forecast; the no-forecast leg keeps 0.20 (= 0.30 widened).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-28 15:42:50 +01:00
glenn schrooyenandClaude 382b0ea411 TARIFF-24 round 2: a usable solar forecast replaces the "no weather" x1.5 (AUDIT-02 F1)
With battery modes on, the price gap a mode needs was always widened x1.5 by
Uncertainty.NO_WEATHER, even with a solar forecast read, so a 0.08 day/night
gap never traded at the shipped cycle cost. The doubt is now added only when
no usable forecast is read (solar_fc is None, the day plan's own test), in the
run plan (_price_runs), the charge gate (_mode_roles) and Home's mirror
(homeview.trade_verdict). The rule is one helper, arbitrage.weather_uncertainty();
the modes-off trade path (main.py cycle) keeps its literal NO_WEATHER.

DOCS "Battery modes" sentence, CHANGELOG 0.19.1 line and the DECISIONS note
follow. Tests: test_tariff24_weather.py.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-28 15:08:21 +01:00
glenn schrooyenandClaude 230ba0b57a TARIFF-24: test file follows the repo's suite rules (MUTATION.md pointer, named absence)
checks / unit (pull_request) Successful in 49m23s
infra-pin / fresh (pull_request) Successful in 2s
Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-28 01:09:39 +01:00
glenn schrooyenandClaude d667f3f3a8 TARIFF-24: battery modes start the tariff task (hotfix 0.19.1)
With battery modes on, a day/night or fixed static_profile, no price feed and
arbitrage off, tariff_refresh_wanted() was false: no tariff task, no
tariff_plan, mode_runs None, so Leftover solar / Cheap hours never charged from
the grid and the Batteries screen said "No prices". packmode.enabled(opts) is
now a third term of the gate. With price_provider off the task is pure
arithmetic on static_profile, so no HTTPS call is added.

Also: version 0.19.1, CHANGELOG entry, DECISIONS note, test_tariff24.py, and a
shorter tariff_refresh_wanted() docstring (history stays in DECISIONS).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-28 01:02:32 +01:00
admin 4a1f6cc2d9 Merge pull request 'UX-54: a Forecast tab with the sun, the house and each battery's level, per hour' (#210) from UX-54 into release/1.0
infra-pin / fresh (push) Successful in 2s
2026-10-27 21:07:00 +01:00
glenn schrooyenandClaude a0ace389a4 UX-54 round 6: the floor cut in one step, never from an empty battery
checks / unit (pull_request) Successful in 50m21s
infra-pin / fresh (pull_request) Successful in 1s
daycurve._floor_cut() set a cut from a battery already at the floor (its
cap 0 is below its rating), so a give run with one battery empty and the
other held to its rating was cut until MAX_FLOOR_CUTS ran out: 2,800 cuts
and 460 ms a build, and give blocks back at the hour edge after that.

The shortfall in a step is now shared as control shares it (by what each
battery holds above the floor, packshare.weights(), each at most its
rating, the rest passed on), in the step and in the cut. A battery giving
v kW that holds h kWh is empty at h / v; one at the floor holds nothing and
never sets a cut. The cut lands where the step then comes out exact, so it
is placed once. curve(stats=) reports the floor cuts and whether the bound
stopped one.

test_ux54.py: _floor_cut() checked to the second against floor times worked
out by hand (one battery, a group with one held to its rating, the second
group, the split by what they hold), a battery at the floor sets no cut,
the bound is reported; own_walk() with the reviewer's two cases (30/80 %,
guard, 7 kW house; 2 x 3 kW, 3.5 kW house) and, per build, the floor cuts,
the level points and the time.

No decision, command, timing or option key changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 19:03:56 +01:00
glenn schrooyenandClaude a4b769f257 UX-54 round 5: test_ux54_dst.py points at MUTATION.md (test_bytecode.py)
checks / unit (pull_request) Successful in 50m12s
infra-pin / fresh (pull_request) Successful in 1s
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 16:19:07 +01:00
glenn schrooyenandClaude 8dbf831854 UX-54 round 5: give blocks end at the floor point, the tab on the clock-change days
checks / unit (pull_request) Failing after 50m11s
infra-pin / fresh (pull_request) Successful in 1s
daycurve.curve() puts a point where a giving battery reaches the floor
inside a step (_floor_cut(), as it already does for a grid charge reaching
its level), so _stops() ends a give block in the quarter control stops. A
walk where control moves the packs with its own model (not the curve's
levels) matches the tab quarter by quarter in 11 cases; 25/45 % with the
peak guard: control 17:00-20:15, block 17:00-20:06.

SolarToday keys an hour by its start taken back from the UTC instant, so
the two 02:00 hours of the autumn change stay apart. test_ux54_dst.py builds
the tab (forecast_payload()) at every quarter of 27-29 March and
30 October-1 November 2027, modes on and off, in the no-tzdata zone and,
where the host has tzdata, ZoneInfo, every call under a time limit raised
as a BaseException that forecast_payload() cannot swallow.

give_later: "No block shows while a battery has no energy to give." /
"Er verschijnt geen blok zolang een batterij niets kan leveren."
DECISIONS: what the walks prove, the modes-off charge drawn on a sunny day,
the clock-change days.

No decision, command, timing or option key changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 15:54:33 +01:00
glenn schrooyenandClaude 00c8ae354c Merge release/1.0 (f5afb42, SAFETY-123) into UX-54
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 15:30:52 +01:00
adminandClaude Opus 5.5 f5afb420cf Merge pull request 'SAFETY-123: the day plan's hour loop steps in UTC and always ends' (#211) from SAFETY-123 into release/1.0
infra-pin / fresh (push) Successful in 2s
Reviewed head d2820e8 (Opus review; CI run 1408 checks / unit and 1409 infra-pin green on d2820e8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:00:21 +01:00
glenn schrooyenandClaude d2820e8b9b SAFETY-123: the day plan's hour loop steps in UTC and always ends
checks / unit (pull_request) Successful in 48m40s
infra-pin / fresh (pull_request) Successful in 2s
dayplan._hour_slices() added an hour to the local wall clock and read it
back. Without tzdata (tariff._EUTimezone) 02:00 on the spring change reads
as 00:00 UTC again, so the loop never advanced and the list grew until the
process died. Each step now adds the time left in the local hour to the UTC
instant. Ordinary days give the old slices; the spring day gives 23 slices
and the autumn day 25. No option, no text change.

test_safety123.py: every quarter of 27-29 March and 30 Oct-1 Nov 2027 (and
the US change days) for the slices, dayplan.plan() and
Controller._mode_roles(), each call under a hard time limit.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 08:58:54 +01:00
glenn schrooyenandClaude 4256bd3eda UX-54 round 4: modes off reads tomorrow at Home's count, no relabel in the walk
checks / unit (pull_request) Successful in 48m51s
infra-pin / fresh (pull_request) Successful in 2s
Modes off has no curve (no forecast is read, no house profile kept), so
dayview._levels() never ran there and tomorrow's legs were drawn from the
bound. homeview.trade_runs(ahead=f) now reads each leg at f(name, kind,
start), as mode_runs() does: no give while the emptiest battery is at the
floor, each share sized from that level. dayview._counted() gives a give
the level by Home's own count (the level now, plus what the blocks before
it put in, less what they take out); a charge is read from the floor,
because a full battery covers the house before the cheap hour.

The battery line says a give is planned when one is and the battery has
no give block, also with modes off.

test_ux54.py: the modes-on walk no longer relabels a GIVE role at the
floor; it asks control's own discharge envelope (packshare.envelope())
with each pack's cut-out at the floor. The peak-guard 20 % case asserts
the curve is at the floor at the 17:00 give start. A modes-off walk on a
cloudy day (arbitrage.decide() with the cycle's inputs, the packs moved by
what control does and by the house), cases A-D: control charges and gives
in exactly the quarters the tab has a block. trade_runs(ahead=f) unit
check. _stops() ends a give block where the level the tab shows is at the
floor (LEVEL_DP).

No decision, command, timing or option key changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 03:06:12 +01:00
glenn schrooyenandClaude 5ab8622265 UX-54 round 3: tomorrow's blocks last as long as control acts, give blocks checked at run start
checks / unit (pull_request) Successful in 48m22s
infra-pin / fresh (pull_request) Successful in 1s
- Tomorrow's grid block is drawn to where control stops charging. A cheap run
  is sized for all batteries together; a slower one is still below
  `charge_to()` when it ends and control plans the next cheap slots. The tab
  asks `modetrade.energy()` and `priceruns.plan()` with the packs at the
  curve's level and extends the run, so the curve charges as long.
- Each battery's block ends where it is at its own level (grid) or at the
  floor (give). The curve puts a point where a battery reaches its charge
  level inside a step.
- `homeview.mode_runs(ahead=f, rp=...)` reads each block at the level the
  curve has when it starts, with Home's own filter and MIN_KWH: no give block
  at the floor, no grid block at the planned level. Home never passes either.
- `no_tomorrow` names the hour from `solarfc.BELL_FROM_H`, where the curve
  stops.
- test_ux54.py part 7 compares control (`_mode_roles()` every 15 minutes) with
  the tab with no tolerance, and adds the cloudy / floor case.

No decision, command, timing or option key changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-27 00:53:58 +01:00
glenn schrooyenandClaude 75195f7073 UX-54 round 2: tomorrow's blocks where control will act, trust as the plan counts it, strings
checks / unit (pull_request) Successful in 48m25s
infra-pin / fresh (pull_request) Successful in 1s
F1: tomorrow's blocks no longer depend on the battery's level now. Home sizes
a block from the level now (`room()`, `plan_need()` in mode_runs, the leg
sizing in trade_runs) and draws none when there is no room or energy. For a
day that has not started the builders take `ahead=True` (Home never passes
it): a charge is read from the floor, a give from the ceiling. A grid block is
then kept only where `modetrade.chargers()` would charge at the curve's level
when the run starts. The UX-53 gate is unchanged. Today still equals Home; a
give run in the plan with no block (empty battery) has its own sentence
instead of "No charge or give blocks planned".

F2: test_ux54.py part 7 walks the real `_mode_roles()` every 15 minutes
through tomorrow and compares the roles with the tab's blocks (modes on, the
peak gate, part-full batteries), and the control cycle's verdict at 07:00
(modes off).

F3: the curve counts spare solar as trust x max(0, sun - house), stored at
EFF, as `dayplan.plan()` does (10 kWh sun, 2 kWh house, 70 %: 5.32 kWh).

Strings: cycle, no_tomorrow, ahead, no_level, measured; new give_later.

No decision, command, timing or option key changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-26 22:48:48 +01:00
glenn schrooyenandClaude b6b2d1b184 UX-54: a Forecast tab with the sun, the house and each battery's level, per hour
checks / unit (pull_request) Successful in 48m29s
infra-pin / fresh (pull_request) Successful in 1s
A new tab beside Home and Batteries for today and tomorrow: the solar
forecast, the solar measured so far, the house's expected use and each
battery's expected level, with the plan's own charge and give blocks under it.

- Blocks are Home's: today's are the plan's `runs`, tomorrow's come from the
  same builders (`homeview.runs_for()`), in the plan lines' words.
- The sun is what solarfc reads; a daily total is spread over the bell and the
  tab says so. The house is HouseProfile.estimator(), as the plan reads it.
- The level is new: `daycurve.curve()` gives each battery the role
  `modetrade.decide()` gives it and moves it hour by hour inside the plan's
  floor and ceiling. It is an estimate and counts on `solar_trust_pct`.
- Built on request (`GET forecast`), no Home Assistant call. Measured solar is
  kept per hour in memory from the reading control already takes.
- A part not known yet is a note: modes off, no forecast, learning, no
  forecast for tomorrow, a monthly cycle, no level, stopped, shadow.

No decision, command, timing or option key changes. Found, not changed:
`dayplan.plan()` over-counts a state-only forecast (DECISIONS/UX-54).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-10-26 19:38:59 +01:00
admin a1d07030f8 Merge pull request 'UX-53: Home draws a grid charge only where the add-on will take it' (#209) from UX-53 into release/1.0
infra-pin / fresh (push) Successful in 2s
2026-10-26 13:59:22 +01:00
glenn schrooyenandClaude Opus 5.5 5dfe9720c5 UX-53 review: DECISIONS text, day/night weekend case and peak guard limit
checks / unit (pull_request) Successful in 48m2s
infra-pin / fresh (pull_request) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 13:10:49 +01:00