Fixes OPT-20 (TEST-23 OPT-15 g7, finding F-A): a refusal probe asked the whole trade, and the part B's pin did not hold landed on a battery in its maintenance charge. Base: release/1.0 bf9019e. Everything measured in the real Controller in the lagged replay (goodwe-addon-checks:1, --network none); no TEST-23 use.
Cause
modetrade.decide() set the trade ask to the sum of the ratings of every charger, the probing refuser included (modetrade.py, ask = -sum(abs(p.rating_w) for p in ch)), and size() capped it only by the loop bound and the headroom. B's SAFETY-73 pin (main.pyClaim.set("refused", P_SAFETY, -PROBE_W)) holds 200 W of it; the allocator hands the rest to a unit it can reach. A battery in a maintenance phase gets no mode range (main.py: "never on a battery in any maintenance phase"), so nothing kept the other 2300 W off A. Replayed on bf9019e: A -2300 W for 33 s, grid 3100 W, the trade line "2500 W on inverter-b".
Fix
modetrade.decide(): the ask is PROBE_W for the probing battery plus what the other chargers would have had without the probe (none: PROBE_W alone). Tick.probe_w carries the probe's part.
modetrade.size(): the other chargers' share is sized as before (bound, headroom), the probe's PROBE_W is added on top. A probe alone is PROBE_W exactly, or no trade when the headroom or the bound is under it (a smaller ask would void the pin and the allocator would place the rest elsewhere).
main._mode_trade(): the ask carries PROBE_W for exactly the pins filed this tick. decide() runs before the pin is filed and the window's last cycle has no pin, so on the first and last cycle of a window another battery's share moved by 200 W (a normal charger A: -2300 W instead of -2500 W on the first cycle; a maintenance A: the whole ask, -2500 W, on the cycle after the window).
Probe timing, the refusal-ended detection, the REST roles and everything else OPT-15 does are untouched. No option, no string template.
A -2300 W for 33 s and -2500 W for 1 cycle after, grid 3300 W
A 0 W, B -200 W, grid 800 W
(a') same, maintenance charge shaped to 0 W
same
A 0 W, B -200 W
(b) A idle at 0 W
A 0 W, B -200 W
same
(c) A a normal charger in the run
A -2300 W on the first cycle
A -2500 W throughout, B -200 W
g7 (drain 18:00 to 18:17:04, then maintenance charge at 0 W)
A -2300 W, grid 3300 W
A 0 W, B -200 W, grid 800 W
Not changed (read before review)
Out of scope, untouched: the base P4 F4 path (a normal trade placed on a battery in maintenance charge) and the action-log wording (UX-38). The only visible change in the log: a probe alone says "200 W on inverter-b" where it said "2500 W" (same template, the number is the size). Reported, not widened.
A that gives the house still rests for the window (OPT-11 owner answer 3: while one battery charges from the grid none gives). The grid then rises by what A gave, as on bf9019e (test part 3, case b2). The brief's "at most PROBE_W" holds when A is idle, charging or in maintenance, not when A was giving power. Not a regression; it is the one-sign rule.
Beside a normal charger whose share is headroom-limited, the probe's 200 W sits on top of the headroom for the 33 s window (about 7 W on the quarter) instead of that charger giving 200 W up. Chosen so the charger's command is unchanged (brief, point 1); the guard measures every cycle.
Identity
Modes off: tools/opt11p3_identity.py (6 scenarios, N=1 and 2, late P1 sample) and the pinned digests in test_price_run_fixes: byte-equal to bf9019e.
Modes on, no probe: tools/opt11p4_identity.py (12 scenarios) byte-equal to bf9019e, and tools/opt20_scen.py (3 scripted cheap runs, 1.2 h, 2 inverters, lagged: A Solar first, A charging, A in maintenance charge; every share, grid, plant power, role and trade sentence) pinned in test_opt20.py part 4.
Tests
test_opt20.py: modetrade ask and size (8 checks), the pin sync in _mode_trade (3), the real Controller for (a), (a'), (b), (c), the g7 sequence and (b2) (5 to 6 checks each), identity (1). On bf9019e the replay part fails 12 checks. 12 mutations, each caught by test_opt20.py (table in the dev report). DECISIONS: OPT-11-battery-modes.md, OPT-20 bullet.
Mutations (each caught by test_opt20.py; reds = failed checks there)
#
breakage
test_opt20 reds
others
M1
modetrade.py: the probing battery is asked its whole rating again (the TEST-23 ask)
16
none
M2
modetrade.py: a normal charger counts as a prober (asked PROBE_W)
2
test_modes_p3 1
M3
modetrade.py: a probe under PROBE_W is filed (voids the pin)
1
none
M4
modetrade.py: the probe is not added on top of the other chargers' share
4
none
M5
modetrade.py: the other chargers give 200 W up when headroom binds
1
none
M6
main.py: no pin/ask sync at all
7
none
M7
main.py: sync only when the window has closed (last cycle)
2
none
M8
main.py: sync only when the pin is new (first cycle)
5
none
M9
modetrade.py: the probe is asked twice PROBE_W
3
none
M10
modetrade.py: the bound does not reserve the probe's PROBE_W
1
none
M11
main.py: pins counted by the wrong sign
5
test_price_run_fixes 2
M12
modetrade.py: the tick forgets which part of the ask is the probe
Fixes OPT-20 (TEST-23 OPT-15 g7, finding F-A): a refusal probe asked the whole trade, and the part B's pin did not hold landed on a battery in its maintenance charge. Base: release/1.0 bf9019e. Everything measured in the real Controller in the lagged replay (`goodwe-addon-checks:1`, `--network none`); no TEST-23 use.
## Cause
`modetrade.decide()` set the trade ask to the sum of the ratings of every charger, the probing refuser included (`modetrade.py`, `ask = -sum(abs(p.rating_w) for p in ch)`), and `size()` capped it only by the loop bound and the headroom. B's SAFETY-73 pin (`main.py` `Claim.set("refused", P_SAFETY, -PROBE_W)`) holds 200 W of it; the allocator hands the rest to a unit it can reach. A battery in a maintenance phase gets no mode range (`main.py`: "never on a battery in any maintenance phase"), so nothing kept the other 2300 W off A. Replayed on bf9019e: A -2300 W for 33 s, grid 3100 W, the trade line "2500 W on inverter-b".
## Fix
- `modetrade.decide()`: the ask is PROBE_W for the probing battery plus what the other chargers would have had without the probe (none: PROBE_W alone). `Tick.probe_w` carries the probe's part.
- `modetrade.size()`: the other chargers' share is sized as before (bound, headroom), the probe's PROBE_W is added on top. A probe alone is PROBE_W exactly, or no trade when the headroom or the bound is under it (a smaller ask would void the pin and the allocator would place the rest elsewhere).
- `main._mode_trade()`: the ask carries PROBE_W for exactly the pins filed this tick. `decide()` runs before the pin is filed and the window's last cycle has no pin, so on the first and last cycle of a window another battery's share moved by 200 W (a normal charger A: -2300 W instead of -2500 W on the first cycle; a maintenance A: the whole ask, -2500 W, on the cycle after the window).
- Probe timing, the refusal-ended detection, the REST roles and everything else OPT-15 does are untouched. No option, no string template.
## Results (replay, 1 s cycles, lagged readings)
| case (B refuses, probed) | bf9019e | this branch |
|---|---|---|
| (a) A in its maintenance charge phase | A -2300 W for 33 s and -2500 W for 1 cycle after, grid 3300 W | A 0 W, B -200 W, grid 800 W |
| (a') same, maintenance charge shaped to 0 W | same | A 0 W, B -200 W |
| (b) A idle at 0 W | A 0 W, B -200 W | same |
| (c) A a normal charger in the run | A -2300 W on the first cycle | A -2500 W throughout, B -200 W |
| g7 (drain 18:00 to 18:17:04, then maintenance charge at 0 W) | A -2300 W, grid 3300 W | A 0 W, B -200 W, grid 800 W |
## Not changed (read before review)
- **Out of scope, untouched:** the base P4 F4 path (a normal trade placed on a battery in maintenance charge) and the action-log wording (UX-38). The only visible change in the log: a probe alone says "200 W on inverter-b" where it said "2500 W" (same template, the number is the size). Reported, not widened.
- **A that gives the house still rests for the window** (OPT-11 owner answer 3: while one battery charges from the grid none gives). The grid then rises by what A gave, as on bf9019e (test part 3, case b2). The brief's "at most PROBE_W" holds when A is idle, charging or in maintenance, not when A was giving power. Not a regression; it is the one-sign rule.
- Beside a normal charger whose share is headroom-limited, the probe's 200 W sits on top of the headroom for the 33 s window (about 7 W on the quarter) instead of that charger giving 200 W up. Chosen so the charger's command is unchanged (brief, point 1); the guard measures every cycle.
## Identity
- Modes off: `tools/opt11p3_identity.py` (6 scenarios, N=1 and 2, late P1 sample) and the pinned digests in test_price_run_fixes: byte-equal to bf9019e.
- Modes on, no probe: `tools/opt11p4_identity.py` (12 scenarios) byte-equal to bf9019e, and `tools/opt20_scen.py` (3 scripted cheap runs, 1.2 h, 2 inverters, lagged: A Solar first, A charging, A in maintenance charge; every share, grid, plant power, role and trade sentence) pinned in test_opt20.py part 4.
## Tests
`test_opt20.py`: modetrade ask and size (8 checks), the pin sync in `_mode_trade` (3), the real Controller for (a), (a'), (b), (c), the g7 sequence and (b2) (5 to 6 checks each), identity (1). On bf9019e the replay part fails 12 checks. 12 mutations, each caught by test_opt20.py (table in the dev report). DECISIONS: OPT-11-battery-modes.md, OPT-20 bullet.
## Mutations (each caught by test_opt20.py; reds = failed checks there)
| # | breakage | test_opt20 reds | others |
|---|---|---|---|
| M1 | `modetrade.py`: the probing battery is asked its whole rating again (the TEST-23 ask) | 16 | none |
| M2 | `modetrade.py`: a normal charger counts as a prober (asked PROBE_W) | 2 | test_modes_p3 1 |
| M3 | `modetrade.py`: a probe under PROBE_W is filed (voids the pin) | 1 | none |
| M4 | `modetrade.py`: the probe is not added on top of the other chargers' share | 4 | none |
| M5 | `modetrade.py`: the other chargers give 200 W up when headroom binds | 1 | none |
| M6 | `main.py`: no pin/ask sync at all | 7 | none |
| M7 | `main.py`: sync only when the window has closed (last cycle) | 2 | none |
| M8 | `main.py`: sync only when the pin is new (first cycle) | 5 | none |
| M9 | `modetrade.py`: the probe is asked twice PROBE_W | 3 | none |
| M10 | `modetrade.py`: the bound does not reserve the probe's PROBE_W | 1 | none |
| M11 | `main.py`: pins counted by the wrong sign | 5 | test_price_run_fixes 2 |
| M12 | `modetrade.py`: the tick forgets which part of the ask is the probe | 13 | none |
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
The probe's trade was sized at the probing battery's rating. Its SAFETY-73
pin held 200 W of that, and the allocator put the rest on a battery with no
mode range (one in a maintenance phase): TEST-23 OPT-15 g7, -2300 W on A in
its maintenance charge for 34 s, grid 3100 W.
modetrade.decide() now asks PROBE_W for the probing battery plus what the
other chargers would have had without the probe; size() adds the probe on top
of their share, or files PROBE_W exactly, or nothing. _mode_trade() carries
the pin only on the ticks it is filed, so the window's first and last cycle
do not move another battery's share either. Modes off and modes on with no
probe are unchanged (bf9019e digests). Tests: test_opt20.py.
Co-Authored-By: Claude <noreply@anthropic.com>
test_opt20 part 3 on bf9019e (parts 1, 2 and 4 use the new API): 12 checks fail, the same 12 the dev report names. (b) and (b2) pass there, as expected.
Identity, bf9019e vs ccdb9dc, byte-equal: opt11_identity (9 lines), opt11p3_identity (6, modes off), opt11p4_identity (12, modes on, no probe). opt20_scen.py on bf9019e prints the 3 digests pinned in test_opt20 part 4.
CI on ccdb9dc: run 1289 checks / unit (pull_request) success; run 1290 infra-pin / fresh success.
My own scenarios (real Controller, lagged replay), ccdb9dc vs bf9019e
g7, with the drain ending 10 s into a probe window (on the rig the probe came 2 s after the drain)
A stays at 0 W, grid 800 W at most
A -2500 W, grid 3300 W
R2
A is a normal charger whose share is limited by headroom (house 2600 W)
A stays at about -850 W (±6 W loop noise). Quarters 3451/3451 W
A drops to -672 W. Quarters 3451/3451 W
R3
probe beside A in maintenance charge. The house steps from 800 to 4200 W 10 s into the window (the guard engages)
A stays at 0 W. The trade stops and the pin is voided
A -2500 W, grid 6700 W
R4
probe beside A in a maintenance HOLD
A stays at 0 W
A stays at 0 W
R5
R2, but the house rises 700 W in the middle of the window
A follows the shrinking headroom. Quarter means equal to base
the same
Mutations (mine, not in the dev's table), against test_opt20.py
#
breakage
reds
R3
sync adds the pin's watts but never removes the stale probe (ask_w - pins)
5
R4
sync moves the ask but leaves probe_w unchanged
6
R5
decide sets probe_w but leaves the probe out of the ask
6
R6
size: a probe alone needs more than PROBE_W of room (> for >=)
3
R7
size: the others' share counts the probe in the ask (-ask_w without - pw)
11
R8
size: the total is held under the headroom (min(rest + pw, room), so the other charger gives up 200 W)
1
R10
size: the probe branch is skipped for a single probe (pw > PROBE_W)
2
R1, R2
sync: drop the source == "refused" filter, or the kind == "set" filter
0, equivalent: no other charge SET claim exists at that point (maintenance SETs are drain >0 or hold 0), and refusal limits are not set
Findings (none blocking)
Info (dev report only, not code): the "residual" of a P_PEAK floor clamping the trade below PROBE_W cannot happen today.peakguard.claim() files a floor only while the guard is engaged. That guard is decided this cycle (main.py:4728), before _mode_roles() (main.py:5055). An engaged guard makes charge_ok False (main.py:2361), so no trade is filed in that cycle and the pin is voided at a target >= 0. R3 confirms this: no leak. DECISIONS does not claim the residual, so no change is needed.
Low, test strength: the refusing plant never draws the probe's 200 W, so the replay's "grid up by at most PROBE_W" check covers A's leak only. The design choice of probing beside a headroom-bound charger is covered by the unit size check alone (M5, R8). R2 above shows it holds in the integrated path. Not worth a CI cycle now.
Info: the g7 replay is a re-scripted sequence, not a trace replay. Its first probe after the drain is at 18:31, while the rig's was at 18:19:02, 2 s after the drain. R1 covers that edge: on ccdb9dc A stays at 0 W.
The deviations
(a) A gives power to the house during a probe: A rests for the window and the grid rises by what A gave, the same as on bf9019e. Recommendation: accept, do not skip the probe.
The headroom does not depend on the battery (peakguard wanted), so the probe is only filed when the quarter can take the house without A plus 200 W.
The cost is about 800 W × 34 s ≈ 7.5 Wh per probe, at the cheap-run price.
Skipping the probe while another battery gives would leave a refusing battery unprobed for most of a night cheap run. That is OPT-15 F3 again.
Letting A keep giving while B charges is the opposite-direction split that SAFETY-73 forbids.
(b) The probe sits up to 200 W above the headroom: safe.
The worst case is about 7 W on the quarter, inside the 50 W peak_margin_w. The over-headroom part flows only if B really draws 200 W, and then the probe is answered and the refusal ends.
The headroom is fed forward from the quarter's banked energy, so a charge that continues absorbs the overshoot later in the quarter. R2 and R5 bank the same as base.
The loop bound is never exceeded (rest <= -lo - pw).
(c) P_PEAK clamp: not reproducible (finding 1). At the owner's site (2 × 2500 W) the bound with a probe is -2700 W. The probe alone is filed only with room >= 200 W, and a floor never coexists with a trade. Likelihood: none through P_PEAK today. It would take a new claim above P_ARBITRAGE that clamps a filed charge to between 0 and -200 W.
Nothing pushed.
## Review of OPT-20 at ccdb9dc (base bf9019e)
**Verdict: correct. No blocking findings. Merging with head_commit_id pinned to ccdb9dc.**
### What I ran (goodwe-addon-checks:1, --network none, PYTHONDONTWRITEBYTECODE=1, own uid)
- Full suite on ccdb9dc: **78 of 78 pass**.
- test_opt20 part 3 on bf9019e (parts 1, 2 and 4 use the new API): **12 checks fail**, the same 12 the dev report names. (b) and (b2) pass there, as expected.
- Identity, bf9019e vs ccdb9dc, byte-equal: `opt11_identity` (9 lines), `opt11p3_identity` (6, modes off), `opt11p4_identity` (12, modes on, no probe). `opt20_scen.py` on bf9019e prints the 3 digests pinned in test_opt20 part 4.
- CI on ccdb9dc: run 1289 `checks / unit (pull_request)` success; run 1290 `infra-pin / fresh` success.
### My own scenarios (real Controller, lagged replay), ccdb9dc vs bf9019e
| | scenario | ccdb9dc | bf9019e |
|---|---|---|---|
| R1 | g7, with the drain ending 10 s into a probe window (on the rig the probe came 2 s after the drain) | A stays at 0 W, grid 800 W at most | A -2500 W, grid 3300 W |
| R2 | A is a normal charger whose share is limited by headroom (house 2600 W) | A stays at about -850 W (±6 W loop noise). Quarters 3451/3451 W | A drops to -672 W. Quarters 3451/3451 W |
| R3 | probe beside A in maintenance charge. The house steps from 800 to 4200 W 10 s into the window (the guard engages) | A stays at 0 W. The trade stops and the pin is voided | A -2500 W, grid 6700 W |
| R4 | probe beside A in a maintenance HOLD | A stays at 0 W | A stays at 0 W |
| R5 | R2, but the house rises 700 W in the middle of the window | A follows the shrinking headroom. Quarter means equal to base | the same |
### Mutations (mine, not in the dev's table), against test_opt20.py
| # | breakage | reds |
|---|---|---|
| R3 | sync adds the pin's watts but never removes the stale probe (`ask_w - pins`) | 5 |
| R4 | sync moves the ask but leaves `probe_w` unchanged | 6 |
| R5 | decide sets probe_w but leaves the probe out of the ask | 6 |
| R6 | size: a probe alone needs more than PROBE_W of room (`>` for `>=`) | 3 |
| R7 | size: the others' share counts the probe in the ask (`-ask_w` without `- pw`) | 11 |
| R8 | size: the total is held under the headroom (`min(rest + pw, room)`, so the other charger gives up 200 W) | 1 |
| R10 | size: the probe branch is skipped for a single probe (`pw > PROBE_W`) | 2 |
| R1, R2 | sync: drop the `source == "refused"` filter, or the `kind == "set"` filter | 0, **equivalent**: no other charge SET claim exists at that point (maintenance SETs are drain >0 or hold 0), and refusal limits are not `set` |
### Findings (none blocking)
1. **Info (dev report only, not code): the "residual" of a P_PEAK floor clamping the trade below PROBE_W cannot happen today.** `peakguard.claim()` files a floor only while the guard is engaged. That guard is decided this cycle (main.py:4728), before `_mode_roles()` (main.py:5055). An engaged guard makes `charge_ok` False (main.py:2361), so no trade is filed in that cycle and the pin is voided at a target >= 0. R3 confirms this: no leak. DECISIONS does not claim the residual, so no change is needed.
2. **Low, test strength:** the refusing plant never draws the probe's 200 W, so the replay's "grid up by at most PROBE_W" check covers A's leak only. The design choice of probing beside a headroom-bound charger is covered by the unit `size` check alone (M5, R8). R2 above shows it holds in the integrated path. Not worth a CI cycle now.
3. **Info:** the g7 replay is a re-scripted sequence, not a trace replay. Its first probe after the drain is at 18:31, while the rig's was at 18:19:02, 2 s after the drain. R1 covers that edge: on ccdb9dc A stays at 0 W.
### The deviations
- **(a) A gives power to the house during a probe:** A rests for the window and the grid rises by what A gave, the same as on bf9019e. **Recommendation: accept, do not skip the probe.**
- The headroom does not depend on the battery (peakguard `wanted`), so the probe is only filed when the quarter can take the house without A plus 200 W.
- The cost is about 800 W × 34 s ≈ 7.5 Wh per probe, at the cheap-run price.
- Skipping the probe while another battery gives would leave a refusing battery unprobed for most of a night cheap run. That is OPT-15 F3 again.
- Letting A keep giving while B charges is the opposite-direction split that SAFETY-73 forbids.
- **(b) The probe sits up to 200 W above the headroom:** safe.
- The worst case is about 7 W on the quarter, inside the 50 W `peak_margin_w`. The over-headroom part flows only if B really draws 200 W, and then the probe is answered and the refusal ends.
- The headroom is fed forward from the quarter's banked energy, so a charge that continues absorbs the overshoot later in the quarter. R2 and R5 bank the same as base.
- The loop bound is never exceeded (`rest <= -lo - pw`).
- **(c) P_PEAK clamp:** not reproducible (finding 1). At the owner's site (2 × 2500 W) the bound with a probe is -2700 W. The probe alone is filed only with room >= 200 W, and a floor never coexists with a trade. Likelihood: none through P_PEAK today. It would take a new claim above P_ARBITRAGE that clamps a filed charge to between 0 and -200 W.
Nothing pushed.
admin
merged commit 004f859ec7 into release/1.02026-10-15 14:56:17 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes OPT-20 (TEST-23 OPT-15 g7, finding F-A): a refusal probe asked the whole trade, and the part B's pin did not hold landed on a battery in its maintenance charge. Base: release/1.0
bf9019e. Everything measured in the real Controller in the lagged replay (goodwe-addon-checks:1,--network none); no TEST-23 use.Cause
modetrade.decide()set the trade ask to the sum of the ratings of every charger, the probing refuser included (modetrade.py,ask = -sum(abs(p.rating_w) for p in ch)), andsize()capped it only by the loop bound and the headroom. B's SAFETY-73 pin (main.pyClaim.set("refused", P_SAFETY, -PROBE_W)) holds 200 W of it; the allocator hands the rest to a unit it can reach. A battery in a maintenance phase gets no mode range (main.py: "never on a battery in any maintenance phase"), so nothing kept the other 2300 W off A. Replayed onbf9019e: A -2300 W for 33 s, grid 3100 W, the trade line "2500 W on inverter-b".Fix
modetrade.decide(): the ask is PROBE_W for the probing battery plus what the other chargers would have had without the probe (none: PROBE_W alone).Tick.probe_wcarries the probe's part.modetrade.size(): the other chargers' share is sized as before (bound, headroom), the probe's PROBE_W is added on top. A probe alone is PROBE_W exactly, or no trade when the headroom or the bound is under it (a smaller ask would void the pin and the allocator would place the rest elsewhere).main._mode_trade(): the ask carries PROBE_W for exactly the pins filed this tick.decide()runs before the pin is filed and the window's last cycle has no pin, so on the first and last cycle of a window another battery's share moved by 200 W (a normal charger A: -2300 W instead of -2500 W on the first cycle; a maintenance A: the whole ask, -2500 W, on the cycle after the window).Results (replay, 1 s cycles, lagged readings)
bf9019eNot changed (read before review)
bf9019e(test part 3, case b2). The brief's "at most PROBE_W" holds when A is idle, charging or in maintenance, not when A was giving power. Not a regression; it is the one-sign rule.Identity
tools/opt11p3_identity.py(6 scenarios, N=1 and 2, late P1 sample) and the pinned digests in test_price_run_fixes: byte-equal tobf9019e.tools/opt11p4_identity.py(12 scenarios) byte-equal tobf9019e, andtools/opt20_scen.py(3 scripted cheap runs, 1.2 h, 2 inverters, lagged: A Solar first, A charging, A in maintenance charge; every share, grid, plant power, role and trade sentence) pinned in test_opt20.py part 4.Tests
test_opt20.py: modetrade ask and size (8 checks), the pin sync in_mode_trade(3), the real Controller for (a), (a'), (b), (c), the g7 sequence and (b2) (5 to 6 checks each), identity (1). Onbf9019ethe replay part fails 12 checks. 12 mutations, each caught by test_opt20.py (table in the dev report). DECISIONS: OPT-11-battery-modes.md, OPT-20 bullet.Mutations (each caught by test_opt20.py; reds = failed checks there)
modetrade.py: the probing battery is asked its whole rating again (the TEST-23 ask)modetrade.py: a normal charger counts as a prober (asked PROBE_W)modetrade.py: a probe under PROBE_W is filed (voids the pin)modetrade.py: the probe is not added on top of the other chargers' sharemodetrade.py: the other chargers give 200 W up when headroom bindsmain.py: no pin/ask sync at allmain.py: sync only when the window has closed (last cycle)main.py: sync only when the pin is new (first cycle)modetrade.py: the probe is asked twice PROBE_Wmodetrade.py: the bound does not reserve the probe's PROBE_Wmain.py: pins counted by the wrong signmodetrade.py: the tick forgets which part of the ask is the probeCo-Authored-By: Claude noreply@anthropic.com
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Review of OPT-20 at
ccdb9dc(basebf9019e)Verdict: correct. No blocking findings. Merging with head_commit_id pinned to
ccdb9dc.What I ran (goodwe-addon-checks:1, --network none, PYTHONDONTWRITEBYTECODE=1, own uid)
ccdb9dc: 78 of 78 pass.bf9019e(parts 1, 2 and 4 use the new API): 12 checks fail, the same 12 the dev report names. (b) and (b2) pass there, as expected.bf9019evsccdb9dc, byte-equal:opt11_identity(9 lines),opt11p3_identity(6, modes off),opt11p4_identity(12, modes on, no probe).opt20_scen.pyonbf9019eprints the 3 digests pinned in test_opt20 part 4.ccdb9dc: run 1289checks / unit (pull_request)success; run 1290infra-pin / freshsuccess.My own scenarios (real Controller, lagged replay),
ccdb9dcvsbf9019eccdb9dcbf9019eMutations (mine, not in the dev's table), against test_opt20.py
ask_w - pins)probe_wunchanged>for>=)-ask_wwithout- pw)min(rest + pw, room), so the other charger gives up 200 W)pw > PROBE_W)source == "refused"filter, or thekind == "set"filtersetFindings (none blocking)
peakguard.claim()files a floor only while the guard is engaged. That guard is decided this cycle (main.py:4728), before_mode_roles()(main.py:5055). An engaged guard makescharge_okFalse (main.py:2361), so no trade is filed in that cycle and the pin is voided at a target >= 0. R3 confirms this: no leak. DECISIONS does not claim the residual, so no change is needed.sizecheck alone (M5, R8). R2 above shows it holds in the integrated path. Not worth a CI cycle now.ccdb9dcA stays at 0 W.The deviations
bf9019e. Recommendation: accept, do not skip the probe.wanted), so the probe is only filed when the quarter can take the house without A plus 200 W.peak_margin_w. The over-headroom part flows only if B really draws 200 W, and then the probe is answered and the refusal ends.rest <= -lo - pw).Nothing pushed.