OPT-15: price-run fixes (quarter edge, charge-end overshoot, refusal probe) #157

Merged
admin merged 4 commits from opt15-price-run-fixes into release/1.0 2026-10-13 15:04:12 +02:00
Owner

Fixes the three P3 findings from the OPT-11 P3 post-merge test on TEST-23 (5a4cdc2). Rebased by merge onto release/1.0 e3e4ba6 (release/1.0 merged in, head 065792a; no rebase, no force push). Every number below was measured on e3e4ba6 + this branch. No TEST-23 use: everything is the real Controller in the replay harness (goodwe-addon-checks:1, --network none).

What the merge changed (read this first)

  • Conflicts: 5 files, 9 hunks, both sides kept (p1.py held_t beside held_since; modetrade.py chargers(targets, probe) and a drain clearing the probe; main.py the roll inside SAFETY-90/91's _peak_proj chain, the hold beside SAFETY-95's drain-tail grid_w, P4 state beside the hold state; DOCS and DECISIONS both lines).
  • F1 at two inverters is now also done by SAFETY-91 (next_quarter() grades the next quarter past a block's end). With the old projection two inverters no longer stop at an edge, and rolling gives the same banked quarters (test_price_run_fixes part 1b). The roll is still what fixes one inverter, where the old projection stops the charge at every edge. It stays as designed; nothing is wrong or redundant enough to change the approach.
  • SAFETY-81 x F1. With modes on, the roll also removes the 1 s 0 W command at each quarter edge that tools/safety81_sim.py showed at one inverter, so that test's modes-on digests moved (re-pinned, with a note). Modes off is byte-identical to e3e4ba6 (new section 1b pins it). The alarm needed one adaptation: a rolled projection is not graded until the quarter's first sample, as the old "no seconds left" tick was not, so a standing overload is still notified once per quarter (M16).
  • test_modes_p4 part 1: the six scenarios with a grid charge (_ch) are re-pinned. With the hold and its reseed switched off they equal 5a4cdc2's again (roll and probe do not touch them); the others never moved.
  • test_price_run_fixes part 5: identity pins now e3e4ba6's. n1 and n2 are still 5a4cdc2's; n1_lag/n2_lag moved with SAFETY-90/91 (n2_lag now equals n2).

F1 — charge stopped at a quarter edge and latched (medium)

Cause (reproduced): the P1 averager opens a quarter only when a sample arrives and the HA source builds it 0.35 s after the reading, so the cycle on the boundary tick still saw the old block: "no seconds left", charge_headroom_w() None, "no trade (headroom)", and the latch (keyed to the wall-clock quarter) held the NEW quarter off.
Fix: with modes on, the guard projects the quarter now is in (PeakTracker.project(roll=True), crediting the held sample as QuarterAverager.add() will, under max_hold_s). Second layer: only a measured stop latches; the first unmeasured one in a quarter stops that tick only, the second latches (a headroom missing every other cycle flipped the charge 1799 times in 30 min before this).
On e3e4ba6: one inverter, old projection: a stop at each of the 6 edges crossed (headroom); rolled: none. Two inverters: none either way, banked quarters equal. Charge start still needs more than 500 W headroom; a real mid-quarter guard stop still latches.

F2 — charge-end export overshoot (medium)

Fix: after the reseed at E the loop is held there (no error integrated) until the meter reaches the target in the step's direction, EDGE_HOLD_S (18 s, up to 36 s while the battery reading still moves toward E), a new trade, or another claim moves the command. The replay gained Lag (command dead time, meter and battery reading age, P1 debounce, 1 s cycles).

charge end, 2.5 kW under an 800 W house before after
TEST-23 measured (5a4cdc2) −1120 W (0.45×) —
model, 5 s, instant readings −257 W −10 W
lagged, meter 2 s, hold removed (= the old reseed) −1370 W −10 W
lagged variants (8: 1 s / 2 s cycles, meter 1/2/5 s, fresh battery, slow 8 s battery) — −10 … −51 W
Bound 0.15 × rating = 375 W; no import above the charge's own; settle ≤ 14 s (60 s for the slow battery). Unchanged from the numbers on 5a4cdc2.

Can the hold let a rising house push the quarter over the cap? No.

Lagged replay, two inverters, Cheap hours A charges, Solar first B takes over; house rises +1, +2, +3 kW at the charge end, edge placed 30 s to 15 s before the quarter's end. Banked quarter containing the edge (the next quarter is at most 144 W). Aim 3450 W, cap 3500 W.

plant, house 2000 W (charge sized to the aim) rise e3e4ba6 head, no hold head
1 s plant, edge 03:59:00 (840 s) +1 / +2 / +3 kW 3240.6 / 3252.2 / 3317.0 3242.4 / 3251.5 / 3316.3 3234.5 / 3261.3 / 3326.1
1 s plant, edge 03:59:45 (885 s) +1 / +2 / +3 kW 3405.9 / 3409.7 / 3424.6 3406.3 / 3409.1 / 3423.9 3404.5 / 3415.7 / 3430.5
slow battery (8 s, 2 s dead), 5 s meter, edge 885 s +1 / +2 / +3 kW 3444.1 / 3450.1 / 3456.0 3443.5 / 3449.4 / 3455.4 3444.2 / 3450.1 / 3456.1
Other edges (30 s, 300 s, 600 s) are lower. The hold adds at most +33 W to a quarter against the loop without it (+33 W at house 800 W, +10 W at house 2000 W, +22 W with the slow plant); the highest quarter is 3430.5 W (1 s plant) and 3456.1 W (slow plant, the lag's own 1.4–6 W over the aim, same on e3e4ba6), 44 W under the cap. Why: the import during the hold is the rise, below the charge's own import it replaced; a rise past that is the peak floor's, which ends the hold at once (800 → 4500 W: grid 2000 W ten seconds later). One inverter: the hold is a no-op there (E is 0 W for a Cheap-hours battery), head = no hold, highest 3451.3 W (the known lag overshoot).

F3 — refusal inside a cheap run never probed (low)

Probe, as the card says. 200 W for ~34 s every 15 min under the same gates (guard clear, measured headroom > 500 W, not latched, idle, cheap hours), the others rest; an answered probe wins back the run. Outside the probe window the refusal wins. At one inverter SAFETY-52 already ends a charge refusal. On the new base: a monthly drain (SAFETY-78) comes first, and with a day plan (P4) a probing Leftover solar battery goes only to its planned level; energy() never counts a refuser, so run planning is unchanged.

Interaction with what landed since 5a4cdc2

change interacts?
SAFETY-90 (gap fill), SAFETY-91 (next_quarter) Yes. The roll is inside the _peak_proj chain; at two inverters it duplicates next_quarter() (equal banked quarters), at one it is what fixes F1. Gap fill runs on the rolled projection (M21).
SAFETY-81 (Load too high alarm) Yes, see above: modes-on digests re-pinned, alarm restart kept (M16).
SAFETY-78/84/93/95/96/103, MAINT-13/14/15 (maintenance drain/charge/hold, _charge_gate_open, maintenance_charge_floor) No change to those paths. A drain clears the chargers, so no probe and no trade; a claim that moves the command ends the hold at once (_edge_grid(end), unit-tested; not run as a maintenance scenario); SAFETY-95's drain tail keeps its own grid, the hold sits on the other branch (M18). peak_fc and the charge floor read the forecast sensor, not the projection. SAFETY-111 is not in e3e4ba6 (no code, DECISIONS or PR), so not checked.
OPT-11 P4, OPT-17 (day plan, forecast lists) The plan's levels (charge_to) now apply to a probing refuser; energy() and run sizing unchanged. The forecast lists feed dayplan only. test_modes_p4 modes-on _ch pins re-pinned (hold only).
SAFETY-107 (refusal log, one inverter) No. N=1 only; F3's probe reads inv.probe_from (N>1).
SAFETY-73 probe pin Reused: the probe is the existing pin; F3 only makes the refuser a charger in its window.
SAFETY-112 (waiting for HA) No. Display only; a skipped cycle lets the hold run out on the clock.

Tests

test_price_run_fixes.py (parts 1–5 as before, plus: F1 at one inverter and the two-inverter equivalence, drain vs probe, the day plan vs probe), test_modes_p3.py, test_safety81.py (+ modes-off pins), test_modes_p4.py (re-pins). Full suite in goodwe-addon-checks:1 (--network none, PYTHONDONTWRITEBYTECODE=1): 74/74 pass on the merged head.
Identity, tools/opt11p3_identity.py, e3e4ba6 and the merged head, equal in all six: n1 71245b22…, n2 56cf2801…, n1_off 71245b22…, n2_off 56cf2801…, n1_lag 86ef0f5a…, n2_lag 56cf2801…. SAFETY-81 sim modes off: const4500, edge4600, step6500, burst_end, twice7000, sine4500_fast equal.

Mutations (fresh copies, graded over test_price_run_fixes / test_modes_p3 unless noted; reds)

# mutation reds
M1 main: roll=False 1 / 0
M2 main: an unmeasured headroom latches 2 / 0
M3 main: a measured headroom stop does not latch 1 / 1
M4 tariff: rolled projection credits past max_hold_s 1 / 0
M5 tariff: rolled projection drops the held sample 1 / 0
M6 main: no run-edge hold 3 / 1
M7 main: hold ignores the meter reaching the target 2 / 0
M8 main: hold never times out 2 / 0
M9 main: no extension while the battery moves 2 / 0
M10 main: a new trade / another claim does not end the hold 2 / 0
M11 modetrade: probe=False 5 / 0
M12 modetrade: a refuser is a charger outside its probe 2 / 1
M13 modetrade: a probing battery in maintenance is a charger 1 / 0
M14 main: roll=True (leaks into modes off) 1 / 0
M15 main: a refuser counts as probing all the time 1 / 0 (red since the review test; was equivalent on the first round)
M16 merge: SAFETY-81 alarm grades the rolled projection (test_safety81) 4
M17 merge: a drain no longer stops a probing refuser's charge 1 / 0
M18 merge: the SAFETY-95 line reads self.grid on its non-tail branch, bypassing the hold 3 / 0
M19 merge: P4 targets dropped from the decide() chargers call (+ test_modes_p4) 1 / 0 + 4
M20 merge: the reseed's own move ends the hold at once (moved = False removed) 2 / 1
M21 merge: SAFETY-90 gap fill skipped on the projection (+ test_safety90) 3 / 0 + 29

Not fixed (same on e3e4ba6), for the board

  • With lagged readings a headroom-sized quarter banks up to 1.4 W over the aim (48.6 W under the cap); with a slow 8 s battery up to 6 W over (44 W under).
  • A battery reading older than the meter can make a charge start look like a house over the aim: a real guard stop, latched, plus a −1700 W edge in that lag variant.

DOCS (Battery modes, three lines) and DECISIONS/OPT-11-battery-modes.md (OPT-15 item) updated; no new user-facing text.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

Fixes the three P3 findings from the OPT-11 P3 post-merge test on TEST-23 (5a4cdc2). **Rebased by merge onto release/1.0 e3e4ba6** (`release/1.0` merged in, head 065792a; no rebase, no force push). Every number below was measured on e3e4ba6 + this branch. No TEST-23 use: everything is the real Controller in the replay harness (`goodwe-addon-checks:1`, `--network none`). ## What the merge changed (read this first) - **Conflicts: 5 files, 9 hunks, both sides kept** (`p1.py` `held_t` beside `held_since`; `modetrade.py` `chargers(targets, probe)` and a drain clearing the probe; `main.py` the roll inside SAFETY-90/91's `_peak_proj` chain, the hold beside SAFETY-95's drain-tail `grid_w`, P4 state beside the hold state; DOCS and DECISIONS both lines). - **F1 at two inverters is now also done by SAFETY-91** (`next_quarter()` grades the next quarter past a block's end). With the old projection two inverters no longer stop at an edge, and rolling gives the same banked quarters (`test_price_run_fixes` part 1b). The roll is still what fixes **one inverter**, where the old projection stops the charge at every edge. It stays as designed; nothing is wrong or redundant enough to change the approach. - **SAFETY-81 x F1.** With modes on, the roll also removes the 1 s 0 W command at each quarter edge that `tools/safety81_sim.py` showed at one inverter, so that test's modes-on digests moved (re-pinned, with a note). Modes off is byte-identical to e3e4ba6 (new section 1b pins it). The alarm needed one adaptation: a rolled projection is not graded until the quarter's first sample, as the old "no seconds left" tick was not, so a standing overload is still notified once per quarter (M16). - `test_modes_p4` part 1: the six scenarios with a grid charge (`_ch`) are re-pinned. With the hold and its reseed switched off they equal 5a4cdc2's again (roll and probe do not touch them); the others never moved. - `test_price_run_fixes` part 5: identity pins now e3e4ba6's. `n1` and `n2` are still 5a4cdc2's; `n1_lag`/`n2_lag` moved with SAFETY-90/91 (`n2_lag` now equals `n2`). ## F1 — charge stopped at a quarter edge and latched (medium) **Cause (reproduced):** the P1 averager opens a quarter only when a sample arrives and the HA source builds it 0.35 s after the reading, so the cycle on the boundary tick still saw the old block: "no seconds left", `charge_headroom_w()` None, "no trade (headroom)", and the latch (keyed to the wall-clock quarter) held the NEW quarter off. **Fix:** with modes on, the guard projects the quarter `now` is in (`PeakTracker.project(roll=True)`, crediting the held sample as `QuarterAverager.add()` will, under `max_hold_s`). Second layer: only a *measured* stop latches; the first unmeasured one in a quarter stops that tick only, the second latches (a headroom missing every other cycle flipped the charge 1799 times in 30 min before this). **On e3e4ba6:** one inverter, old projection: a stop at each of the 6 edges crossed (`headroom`); rolled: none. Two inverters: none either way, banked quarters equal. Charge start still needs more than 500 W headroom; a real mid-quarter guard stop still latches. ## F2 — charge-end export overshoot (medium) **Fix:** after the reseed at E the loop is held there (no error integrated) until the meter reaches the target in the step's direction, `EDGE_HOLD_S` (18 s, up to 36 s while the battery reading still moves toward E), a new trade, or another claim moves the command. The replay gained `Lag` (command dead time, meter and battery reading age, P1 debounce, 1 s cycles). | charge end, 2.5 kW under an 800 W house | before | after | |---|---|---| | TEST-23 measured (5a4cdc2) | −1120 W (0.45×) | — | | model, 5 s, instant readings | −257 W | −10 W | | lagged, meter 2 s, hold removed (= the old reseed) | −1370 W | −10 W | | lagged variants (8: 1 s / 2 s cycles, meter 1/2/5 s, fresh battery, slow 8 s battery) | — | −10 … −51 W | Bound 0.15 × rating = 375 W; no import above the charge's own; settle ≤ 14 s (60 s for the slow battery). Unchanged from the numbers on 5a4cdc2. ### Can the hold let a rising house push the quarter over the cap? No. Lagged replay, two inverters, Cheap hours A charges, Solar first B takes over; house rises +1, +2, +3 kW at the charge end, edge placed 30 s to 15 s before the quarter's end. Banked quarter containing the edge (the next quarter is at most 144 W). Aim 3450 W, cap 3500 W. | plant, house 2000 W (charge sized to the aim) | rise | e3e4ba6 | head, no hold | head | |---|---|---|---|---| | 1 s plant, edge 03:59:00 (840 s) | +1 / +2 / +3 kW | 3240.6 / 3252.2 / 3317.0 | 3242.4 / 3251.5 / 3316.3 | 3234.5 / 3261.3 / 3326.1 | | 1 s plant, edge 03:59:45 (885 s) | +1 / +2 / +3 kW | 3405.9 / 3409.7 / 3424.6 | 3406.3 / 3409.1 / 3423.9 | 3404.5 / 3415.7 / **3430.5** | | slow battery (8 s, 2 s dead), 5 s meter, edge 885 s | +1 / +2 / +3 kW | 3444.1 / 3450.1 / 3456.0 | 3443.5 / 3449.4 / 3455.4 | 3444.2 / 3450.1 / **3456.1** | Other edges (30 s, 300 s, 600 s) are lower. The hold adds at most +33 W to a quarter against the loop without it (+33 W at house 800 W, +10 W at house 2000 W, +22 W with the slow plant); the highest quarter is 3430.5 W (1 s plant) and 3456.1 W (slow plant, the lag's own 1.4–6 W over the aim, same on e3e4ba6), 44 W under the cap. Why: the import during the hold is the rise, below the charge's own import it replaced; a rise past that is the peak floor's, which ends the hold at once (800 → 4500 W: grid 2000 W ten seconds later). One inverter: the hold is a no-op there (E is 0 W for a Cheap-hours battery), head = no hold, highest 3451.3 W (the known lag overshoot). ## F3 — refusal inside a cheap run never probed (low) **Probe, as the card says.** 200 W for ~34 s every 15 min under the same gates (guard clear, measured headroom > 500 W, not latched, idle, cheap hours), the others rest; an answered probe wins back the run. Outside the probe window the refusal wins. At one inverter SAFETY-52 already ends a charge refusal. **On the new base:** a monthly drain (SAFETY-78) comes first, and with a day plan (P4) a probing Leftover solar battery goes only to its planned level; `energy()` never counts a refuser, so run planning is unchanged. ## Interaction with what landed since 5a4cdc2 | change | interacts? | |---|---| | SAFETY-90 (gap fill), SAFETY-91 (`next_quarter`) | Yes. The roll is inside the `_peak_proj` chain; at two inverters it duplicates `next_quarter()` (equal banked quarters), at one it is what fixes F1. Gap fill runs on the rolled projection (M21). | | SAFETY-81 (Load too high alarm) | Yes, see above: modes-on digests re-pinned, alarm restart kept (M16). | | SAFETY-78/84/93/95/96/103, MAINT-13/14/15 (maintenance drain/charge/hold, `_charge_gate_open`, `maintenance_charge_floor`) | No change to those paths. A drain clears the chargers, so no probe and no trade; a claim that moves the command ends the hold at once (`_edge_grid(end)`, unit-tested; not run as a maintenance scenario); SAFETY-95's drain tail keeps its own grid, the hold sits on the other branch (M18). `peak_fc` and the charge floor read the forecast sensor, not the projection. SAFETY-111 is not in e3e4ba6 (no code, DECISIONS or PR), so not checked. | | OPT-11 P4, OPT-17 (day plan, forecast lists) | The plan's levels (`charge_to`) now apply to a probing refuser; `energy()` and run sizing unchanged. The forecast lists feed `dayplan` only. `test_modes_p4` modes-on `_ch` pins re-pinned (hold only). | | SAFETY-107 (refusal log, one inverter) | No. N=1 only; F3's probe reads `inv.probe_from` (N>1). | | SAFETY-73 probe pin | Reused: the probe is the existing pin; F3 only makes the refuser a charger in its window. | | SAFETY-112 (waiting for HA) | No. Display only; a skipped cycle lets the hold run out on the clock. | ## Tests `test_price_run_fixes.py` (parts 1–5 as before, plus: F1 at one inverter and the two-inverter equivalence, drain vs probe, the day plan vs probe), `test_modes_p3.py`, `test_safety81.py` (+ modes-off pins), `test_modes_p4.py` (re-pins). Full suite in `goodwe-addon-checks:1` (`--network none`, `PYTHONDONTWRITEBYTECODE=1`): 74/74 pass on the merged head. Identity, `tools/opt11p3_identity.py`, e3e4ba6 and the merged head, equal in all six: n1 71245b22…, n2 56cf2801…, n1_off 71245b22…, n2_off 56cf2801…, n1_lag 86ef0f5a…, n2_lag 56cf2801…. SAFETY-81 sim modes off: const4500, edge4600, step6500, burst_end, twice7000, sine4500_fast equal. ## Mutations (fresh copies, graded over test_price_run_fixes / test_modes_p3 unless noted; reds) | # | mutation | reds | |---|---|---| | M1 | main: `roll=False` | 1 / 0 | | M2 | main: an unmeasured headroom latches | 2 / 0 | | M3 | main: a measured headroom stop does not latch | 1 / 1 | | M4 | tariff: rolled projection credits past `max_hold_s` | 1 / 0 | | M5 | tariff: rolled projection drops the held sample | 1 / 0 | | M6 | main: no run-edge hold | 3 / 1 | | M7 | main: hold ignores the meter reaching the target | 2 / 0 | | M8 | main: hold never times out | 2 / 0 | | M9 | main: no extension while the battery moves | 2 / 0 | | M10 | main: a new trade / another claim does not end the hold | 2 / 0 | | M11 | modetrade: `probe=False` | 5 / 0 | | M12 | modetrade: a refuser is a charger outside its probe | 2 / 1 | | M13 | modetrade: a probing battery in maintenance is a charger | 1 / 0 | | M14 | main: `roll=True` (leaks into modes off) | 1 / 0 | | M15 | main: a refuser counts as probing all the time | 1 / 0 (red since the review test; was equivalent on the first round) | | M16 | merge: SAFETY-81 alarm grades the rolled projection (`test_safety81`) | 4 | | M17 | merge: a drain no longer stops a probing refuser's charge | 1 / 0 | | M18 | merge: the SAFETY-95 line reads `self.grid` on its non-tail branch, bypassing the hold | 3 / 0 | | M19 | merge: P4 `targets` dropped from the decide() chargers call (+ `test_modes_p4`) | 1 / 0 + 4 | | M20 | merge: the reseed's own move ends the hold at once (`moved = False` removed) | 2 / 1 | | M21 | merge: SAFETY-90 gap fill skipped on the projection (+ `test_safety90`) | 3 / 0 + 29 | ## Not fixed (same on e3e4ba6), for the board - With lagged readings a headroom-sized quarter banks up to 1.4 W over the aim (48.6 W under the cap); with a slow 8 s battery up to 6 W over (44 W under). - A battery reading older than the meter can make a charge start look like a house over the aim: a real guard stop, latched, plus a −1700 W edge in that lag variant. DOCS (Battery modes, three lines) and DECISIONS/OPT-11-battery-modes.md (OPT-15 item) updated; no new user-facing text. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-09-29 20:23:12 +02:00
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
fae6681e19
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>
admin added 1 commit 2026-09-29 22:31:54 +02:00
OPT-15 review: bound unmeasured charge stops, test the hold's rules and M15
checks / unit (pull_request) Failing after 3s
infra-pin / fresh (pull_request) Successful in 1s
c38dd1e969
- A second unmeasured-headroom stop in a quarter latches the charge: a
  headroom missing every other cycle flipped it 1799 times in 30 min.
- Unit test of the run-edge hold's end conditions and of a small step.
- Outside its probe a refuser does not make the others rest (M15 red).
- DECISIONS: one review line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
admin added 1 commit 2026-10-13 12:16:42 +02:00
Merge release/1.0 (e3e4ba6) into opt15-price-run-fixes
infra-pin / fresh (pull_request) Successful in 3s
checks / unit (pull_request) Successful in 39m30s
065792aad3
Conflicts (5 files, 9 hunks) resolved keeping both sides:
- p1.py: held_t (OPT-15) beside held_since (SAFETY-90).
- modetrade.py: chargers() takes targets (P4) and probe (OPT-15); decide()
  clears the chargers, a probing refuser included, while a battery drains
  (SAFETY-78); the docstring lists both rules.
- main.py: the roll goes into the SAFETY-90/91 projection chain
  (`_peak_proj`); the SAFETY-95 drain-tail grid_w keeps the run-edge hold on
  its other branch; the P4 forecast state and the hold state both stay.
- DOCS.md, DECISIONS: both lines kept, OPT-15 item refreshed.

Adaptations the merge needs:
- SAFETY-81 x F1: a rolled projection is not graded by the saturation alarm
  until the quarter's first sample, as the old edge tick was not, so a
  standing overload is still notified once per quarter.
- test_safety81, test_modes_p4: modes-on pins re-pinned (with a note), modes
  off pinned to e3e4ba6; test_price_run_fixes: identity pins on e3e4ba6,
  F1 reproduced at one inverter (SAFETY-91 already holds two), tests for the
  drain and the day plan against the probe.

Co-Authored-By: Claude <noreply@anthropic.com>
admin added 1 commit 2026-10-13 14:18:53 +02:00
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
7f0f00b01c
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
Author
Owner

Review of 065792a (+ 7f0f00b, tests and DECISIONS only)

Verdict: correct. No blocker. Three low findings for the board (no code change in this PR). I merge once checks / unit is green on 7f0f00b.

What I re-ran (goodwe-addon-checks:1, --network none, PYTHONDONTWRITEBYTECODE=1)

  • Full suite on 065792a, hwsim required: 74/74 pass.
  • Identity (tools/opt11p3_identity.py), e3e4ba6 vs head: all six equal (n1 71245b22…, n2 56cf2801…, n1_off, n2_off, n1_lag 86ef0f5a…, n2_lag 56cf2801…).
  • SAFETY-81 sim, modes off: all 15 scenarios equal to e3e4ba6 (the test pins 6). Modes on: alarm times as pinned (1813 -> 1811 at the quarter edge, ramp6500 1316 -> 1317).
  • test_modes_p4 _ch re-pins: with the old reseed and no hold, the six digests are exactly 5a4cdc2's old pins again. The re-pin is the hold, nothing else.
  • Item 4: re-ran house 2000 W, edges 840 s and 885 s, 1 s plant and slow plant, base / head without hold / head: dev's numbers reproduced exactly (highest 3430.5 W and 3456.1 W; slow plant at 840 s, not in the PR: 3386.4 W head vs 3370.2 W base).
  • Merge: all 9 conflicted hunks keep both sides (remerge-diff read hunk by hunk).

Maintenance during a hold (replayed, real Controller, lagged plant, edge + 3 s)

B drain, B hold, A drain, B maintenance charge, and a drain on the edge tick itself: the hold ends on the tick the maintenance claim appears in every case. With B hold or B charge, the head is smoother than without the hold (no 1600 W import bump).

Findings (low, follow-up cards, not blockers)

  1. A clipped E holds to the timeout. main.py:2492 clips E to the loop bound; the meter then cannot reach the target, so the hold always runs to EDGE_HOLD_S. If the bound lifts during the hold, nothing ends it (moved stays false). Replayed: B's maintenance charge ends 3 s after the edge, and the site stays at 0 W, importing the 800 W house, for 15 s more. Without the hold the loop moves after 1 s. The same self.i_w = held_w (main.py:2520) overwrites a SAFETY-93/95/96 reseed (main.py:5083 and the next two calls) on the next tick. The cost is bounded: at most 18-36 s, the import is below the charge's own, about 13 W on the quarter. Suggested fix: no hold when E was clipped, or end the hold when a _reseed_* moves the target.
  2. SAFETY-81 at two inverters. On e3e4ba6 the edge tick at N>=2 was graded on SAFETY-91's next-quarter projection. With modes on it is now skipped (main.py:5877), so the 10 s hold restarts at each edge there too. This changes the alarm cadence only, not control. Not measured, because safety81_sim has one inverter. The comment above that line is true at one inverter only.
  3. Action log during F3 probes. Each probe writes two action-log lines, "ARBITRAGE_CHARGE … 200 W on inverter-a" and "arbitrage idle … no trade (size)" (hint "Trading: waiting for a better price"). That is 8 lines an hour while a refusal stands in a cheap run, in a 40-line log. e3e4ba6 writes none. "BATTERY REFUSES" and "… ENDED" are each written once, so SAFETY-73 and SAFETY-107 are not doubled. It is not a flood, but it is noise.

My mutations (not in M1-M21; graded over test_price_run_fixes / test_modes_p3 / test_safety81)

# mutation reds
X1 tariff: rolled acc credits a held export as negative 0/0/0 at 065792a, 1 with 7f0f00b
X3 main: hold ends on abs(grid - tg) < db (no step direction) 1/0/0
X4 main: "moving" without "still >= db from E" 0/0/0 at 065792a, 1 with 7f0f00b
X5 main: "moving" never capped at 2 x EDGE_HOLD_S 1/0/0
X6 modetrade: a probing refuser ignores the day plan's level 1/0/0
X8 main: the second unmeasured stop no longer latches 1/0/0
X11 main: steps under 150 W not held 1/0/0
X15 tariff: roll also mid-quarter 0/2/16

Pushed (7f0f00b)

Two checks in test_price_run_fixes.py for X1 and X4. DECISIONS: the slow-battery quarter (3456 W) and the maintenance replay. No code change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

## Review of 065792a (+ 7f0f00b, tests and DECISIONS only) **Verdict: correct. No blocker. Three low findings for the board (no code change in this PR).** I merge once `checks / unit` is green on 7f0f00b. ### What I re-ran (goodwe-addon-checks:1, `--network none`, `PYTHONDONTWRITEBYTECODE=1`) - Full suite on 065792a, hwsim required: **74/74 pass**. - Identity (`tools/opt11p3_identity.py`), e3e4ba6 vs head: **all six equal** (n1 71245b22…, n2 56cf2801…, n1_off, n2_off, n1_lag 86ef0f5a…, n2_lag 56cf2801…). - SAFETY-81 sim, modes off: **all 15 scenarios equal** to e3e4ba6 (the test pins 6). Modes on: alarm times as pinned (1813 -> 1811 at the quarter edge, ramp6500 1316 -> 1317). - `test_modes_p4` `_ch` re-pins: with the old reseed and no hold, the six digests are exactly 5a4cdc2's old pins again. The re-pin is the hold, nothing else. - Item 4: re-ran house 2000 W, edges 840 s and 885 s, 1 s plant and slow plant, base / head without hold / head: dev's numbers reproduced exactly (highest 3430.5 W and 3456.1 W; slow plant at 840 s, not in the PR: 3386.4 W head vs 3370.2 W base). - Merge: all 9 conflicted hunks keep both sides (remerge-diff read hunk by hunk). ### Maintenance during a hold (replayed, real Controller, lagged plant, edge + 3 s) B drain, B hold, A drain, B maintenance charge, and a drain on the edge tick itself: **the hold ends on the tick the maintenance claim appears** in every case. With B hold or B charge, the head is smoother than without the hold (no 1600 W import bump). ### Findings (low, follow-up cards, not blockers) 1. **A clipped E holds to the timeout.** `main.py:2492` clips E to the loop bound; the meter then cannot reach the target, so the hold always runs to `EDGE_HOLD_S`. If the bound lifts during the hold, nothing ends it (`moved` stays false). Replayed: B's maintenance charge ends 3 s after the edge, and the site stays at 0 W, importing the 800 W house, for 15 s more. Without the hold the loop moves after 1 s. The same `self.i_w = held_w` (`main.py:2520`) overwrites a SAFETY-93/95/96 reseed (`main.py:5083` and the next two calls) on the next tick. The cost is bounded: at most 18-36 s, the import is below the charge's own, about 13 W on the quarter. Suggested fix: no hold when E was clipped, or end the hold when a `_reseed_*` moves the target. 2. **SAFETY-81 at two inverters.** On e3e4ba6 the edge tick at N>=2 was graded on SAFETY-91's next-quarter projection. With modes on it is now skipped (`main.py:5877`), so the 10 s hold restarts at each edge there too. This changes the alarm cadence only, not control. Not measured, because `safety81_sim` has one inverter. The comment above that line is true at one inverter only. 3. **Action log during F3 probes.** Each probe writes two action-log lines, "ARBITRAGE_CHARGE … 200 W on inverter-a" and "arbitrage idle … no trade (size)" (hint "Trading: waiting for a better price"). That is 8 lines an hour while a refusal stands in a cheap run, in a 40-line log. e3e4ba6 writes none. "BATTERY REFUSES" and "… ENDED" are each written once, so SAFETY-73 and SAFETY-107 are not doubled. It is not a flood, but it is noise. ### My mutations (not in M1-M21; graded over test_price_run_fixes / test_modes_p3 / test_safety81) | # | mutation | reds | |---|---|---| | X1 | tariff: rolled `acc` credits a held export as negative | 0/0/0 at 065792a, **1** with 7f0f00b | | X3 | main: hold ends on `abs(grid - tg) < db` (no step direction) | 1/0/0 | | X4 | main: "moving" without "still >= db from E" | 0/0/0 at 065792a, **1** with 7f0f00b | | X5 | main: "moving" never capped at 2 x EDGE_HOLD_S | 1/0/0 | | X6 | modetrade: a probing refuser ignores the day plan's level | 1/0/0 | | X8 | main: the second unmeasured stop no longer latches | 1/0/0 | | X11 | main: steps under 150 W not held | 1/0/0 | | X15 | tariff: roll also mid-quarter | 0/2/16 | ### Pushed (7f0f00b) Two checks in `test_price_run_fixes.py` for X1 and X4. DECISIONS: the slow-battery quarter (3456 W) and the maintenance replay. No code change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin merged commit 79dd0770f9 into release/1.0 2026-10-13 15:04:12 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#157