OPT-15 review: pin the rolled export at 0 W and a battery past E; note the maintenance replay
checks / unit (pull_request) Successful in 38m30s
infra-pin / fresh (pull_request) Successful in 2s

Two survivors of the review's mutations get a check each in
test_price_run_fixes.py (X1: a held export credited negative in the rolled
quarter; X4: the hold extended by a reading already past E). DECISIONS: the
slow-battery quarter (3456 W, same on e3e4ba6) and the replay of a
maintenance phase starting during the hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-13 14:18:41 +02:00
co-authored by Claude Opus 5.5
parent 065792aad3
commit 7f0f00b01c
2 changed files with 23 additions and 1 deletions
@@ -127,8 +127,11 @@ P1 is its section 6 P1, and the pull request lists what was measured.
before its end (two inverters, lagged readings): the hold adds at most
+33 W to that quarter against the loop without it, and the most any
quarter banks is 3431 W with the charge sized to the aim (3450 W; the cap
is 3500 W). The import during the hold is the rise, below the charge's
is 3500 W), 3456 W with a slow 8 s battery (the lag's own, the same on
e3e4ba6). The import during the hold is the rise, below the charge's
own, and a rise past that is the peak floor's, which ends the hold.
A maintenance drain, hold or charge that starts during the hold ends it
on that tick (replayed at edge + 3 s).
## P4: the solar forecast and the day plan (2026-09-29)
- Input: a Home Assistant forecast sensor (`pv_forecast_entity`, kWh left
+19
View File
@@ -187,6 +187,22 @@ check("...and a sample older than the hold is not credited: past the hold "
and peakguard.blindness(_gp, 30.0, False) is not None)
check("without `roll` nothing changes (the modes-off path)",
PeakTracker.project(_avg, _now, roll=False) == _old)
# Review of #157 (X1): a held export is credited as 0 W, as `add()` banks it,
# never as a negative that would pull the rolled quarter under the meter.
_exp = QuarterAverager(phases=1, max_hold_s=30.0)
for _s in range(-900, 1, 1):
_exp.add(make_sample("ha_signed", 0.0, 1500.0, phases=1,
ingest_ts=_edge + timedelta(seconds=_s - 0.35),
ingest_mono=0.0))
_ep = PeakTracker.project(_exp, _now, roll=True)
_exp.add(make_sample("ha_signed", 0.0, 1500.0, phases=1,
ingest_ts=_edge + timedelta(seconds=0.6),
ingest_mono=0.0))
_er = PeakTracker.project(_exp, _now)
check("...and a held export rolls in as 0 W, equal to what the averager "
"banks for those seconds",
_ep.block_start == _er.block_start and _ep.projected_w == 0.0
and _ep.elapsed_w == 0.0 and _er.projected_w == 0.0)
# 1b. The lagged replay: 02:00-03:30 local, a Cheap hours battery charging
# under an 800 W house through five quarter edges.
@@ -444,6 +460,9 @@ check("the hold: the loop reads the target while the meter still shows the "
and _held(3300.0, -2500.0, _hs + 1) == (3300.0, False)
and _held(3300.0, -2000.0, _hs + 1) == (-10.0, True)
and _held(3300.0, -2000.0, 2 * _hs + 1) == (3300.0, False))
# Review of #157 (X4): a reading that has passed E is not "still moving".
check("...and a battery reading already past E does not extend the hold",
_held(3300.0, 900.0, _hs + 1, last_b=700.0) == (3300.0, False))
_f = SimpleNamespace(site_batt_w=500.0, grid=90.0, _edge=None,
tuning=ctl.Tuning(target_grid_w=-10.0), site_max_w=5000.0,
loop_bound=lambda claims: None, target=None, i_w=None)