OPT-15: price-run fixes (quarter edge, charge-end overshoot, refusal probe)
checks / unit (pull_request) Successful in 18m49s
infra-pin / fresh (pull_request) Successful in 1s

F1: with battery modes on, the peak guard projects the quarter `now` is in
even before its first P1 sample (PeakTracker.project(roll=True)), and only
a measured stop latches the period. The edge tick no longer reads "no
seconds left", so a charge is not stopped and latched at a quarter edge.

F2: after a run edge the loop is held at E until the meter shows the step
(EDGE_HOLD_S, longer while the battery still moves). The replay gained a
lag model (dead time, reading age, P1 debounce, 1 s cycles) that shows
TEST-23's overshoot without the hold.

F3: a charge refuser's SAFETY-73 probe inside a cheap run is a charger
pinned at PROBE_W, the others rest, so the probe actually runs.

Modes off stays byte-identical to 5a4cdc2 (new late-P1 identity scenarios).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:22:32 +02:00
co-authored by Claude Opus 5.5
parent 5a4cdc273e
commit fae6681e19
10 changed files with 763 additions and 58 deletions
+23 -2
View File
@@ -19,12 +19,14 @@ import hashlib
import os
import sys
import tempfile
import time
from datetime import timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import opt11_identity as rig # noqa: E402
import safety79_identity as s79 # noqa: E402
from app.p1 import make_sample, split_signed # noqa: E402
from app.price import PricePoint, PriceProduct, PriceRead, brussels # noqa
from app.static_tariff import TariffPlan # noqa: E402
@@ -53,6 +55,21 @@ def _feed(c, t):
s79._feed(c, t)
# OPT-15: the P1 sample of each cycle is built this long after the cycle
# reads the grid (the HA source's debounce), so the cycle on a quarter
# boundary still sees the old block, as on TEST-23.
P1_LAG_S = 0.35
def _feed_lag(c, t):
c.tariff_plan, c.tariff_at = PLAN, rig.BASE + timedelta(seconds=t)
imp, exp = split_signed(c.hass.grid())
c.p1.submit(make_sample(
"ha_signed", imp, exp, phases=1,
ingest_ts=rig.BASE + timedelta(seconds=t - P1_LAG_S),
ingest_mono=time.monotonic()))
def load(t):
"""Under the limit, over it, a surplus, a big evening load."""
k = int(t // 600) % 5
@@ -70,10 +87,13 @@ SCENARIOS = {
# The option present and false: an install that saved it off.
"n1_off": dict(n=1, extra={"battery_modes_enabled": False}),
"n2_off": dict(n=2, extra={"battery_modes_enabled": False}),
# OPT-15: every quarter boundary with the old block still open.
"n1_lag": dict(n=1, lag=True),
"n2_lag": dict(n=2, lag=True),
}
def run(n, *, extra=None, dur=DUR):
def run(n, *, extra=None, dur=DUR, lag=False):
opts = dict(s79.P1, **ARB, **(extra or {}))
trade = []
@@ -85,7 +105,8 @@ def run(n, *, extra=None, dur=DUR):
rig.Controller = functools.partial(real, peak_db_path=db)
try:
rows, c = rig.run(n, extra=opts, dur=dur, load=load, soc=soc,
cut=lambda u, t: False, before=_feed,
cut=lambda u, t: False,
before=_feed_lag if lag else _feed,
on_cycle=on_cycle)
finally:
rig.Controller = real