Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-73-refusal-per-inverter.md
glenn schrooyenandClaude Opus 5.5 06aa09e28c
checks / unit (pull_request) Successful in 10m11s
infra-pin / fresh (pull_request) Successful in 1s
SAFETY-73 review: a refusal needs 22 s of evidence, and the cut-out warning is for giving only
- REFUSED_SPAN_S: the 12 cycles of evidence must also span (SETTLE_CYCLES-1) *
  LOOP_TICK_S = 22 s. At a 1 s meter, lagged healthy readings refused both
  units 10-16 times an hour after load steps (site held 0 W). 2 s and slower
  ticks are unchanged.
- REFUSES_ABOVE_CUT_OUT only for a refusal to give power: a full battery that
  stops taking charge is always above its cut-out level.
- test_delivery: its wedged fixture (0.2 s cycles) no longer reaches a refusal;
  the release/1.0 checks are restored and a span check added.
- test_refusal_units: lagged-reading and full-battery plants, two new checks,
  U14/U15 in the mutation table. Stale "> 500 W" wording fixed. DOCS/DECISIONS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 02:16:24 +02:00

5.7 KiB

SAFETY-73 - a battery that refuses, with two or more inverters

Decided 2026-09-27 by the owner, on the phase A plan (option R). All four new questions answered yes. SAFETY-52's four decisions carry over.

The defect

SAFETY-52 held a refusing battery at 0 W at one inverter only. At N>1 (main.py passed refused=None) nothing closed a unit that gives ~0 W while its state of charge reads above its configured cut-out, so the allocator kept handing it its weighted half. Measured on the real Controller, two units, house 3000 W, A cut out:

register law B gives grid A's gate trips / h
flat -4 W old 533 W +2471 W 46 (each stops the whole site)
flat -4 W new 2000 W (its rating) +1004 W 1
jitter old 1910 W +1094 W 0
jitter new 2000 W +1004 W 0

House 1200 W, which B alone carries: old +671 W (flat) and +284 W (jitter) standing import, new -16 W. Both cut out: old keeps asking two empty batteries (+1910 W each on the jitter shape), new holds the site at 0 W.

What was decided

  1. A refusing unit is closed in that direction by its own P_SAFETY unit claim, source refused. It engages when the unit gives under REFUSED_BELOW_W (100 W) for SETTLE_CYCLES (12) cycles, spanning at least REFUSED_SPAN_S (22 s), in one direction while asked more than PROBE_W (200 W), or when its stale-reading gate trips on a held reading under 100 W. The allocator then hands its share to the others within their own limits (owner decision 1). It is a hard unit limit that always intersects, so OPT-11's P_MODE layer fits inside it.
  2. It binds every strategy's ask on that unit (decision 2): peak guard, arbitrage and maintenance are placed around it like the loop.
  3. A refusal to give power above the configured cut-out logs INFO plus one WARNING ending in REFUSES_ABOVE_CUT_OUT (decision 3). SAFETY-42/43 no longer report such a unit: its share is 0 W, and account() treats it as parked.
  4. All units refusing the same way pass that sign to compute(), so the site holds 0 W with SAFETY-52's law (decision 4). Probes go one unit at a time.
  5. Probe: every 15 min for 34 s the claim becomes a pin at +-200 W and the others carry the rest. The pin stands only while the resolved site target asks at least 200 W its way; otherwise it would make a sibling take the opposite direction, so it falls back to the closed limit for that cycle.
  6. Exits: a reading of 100 W or more in the held direction (probe or not); 100 W or more taken the other way for 12 cycles (charge at the floor; one cycle of the loop overshooting through zero is not enough); stop or start. On every exit the site target and integrator restart from the watts on the wire, or the reopened unit is handed a parked remainder at once.

Why 200 W and not saturation_w. A unit's share is often small: a 400 W night load split two ways asks 200 W of each. With 500 W the empty battery kept a phantom share that SAFETY-43's relative arm then reported as not delivering, the ERROR decision 3 replaces. Shares of 200 W or less that give under 100 W are "too small to call" in account(): the sibling covers the meter and nothing is asked of the empty battery that matters.

Why one direction throughout. On TEST-19's flapping family a command that swaps sign each cycle reads ~0 through the plant's lag: 42 engagements in 21 no-fault runs without the rule, 0 with it.

N=1 is unchanged. Every SAFETY-73 path is gated on len(inverters) > 1. test_refusal_units.py part 6 runs SAFETY-52's whole suite with every SAFETY-73 method made to raise.

Evidence

  • test_refusal_units.py: cases a, b and c on both register shapes against the old behaviour, probes and their spacing, one-at-a-time probing, the recovery reseed, each exit, the small-ask and small-site-target cases, the WARNING, the status row and shadow mode. Mutations U1-U13 in its docstring.
  • No false engagements. TEST-19's no-fault campaign (admin/goodwe sim/delivery_window.py families, 2640 runs at N=2 and 2640 at N=1, every unit executing its share) graded with the engage clause: 0 engagements, the longest qualifying streak 5 cycles against 12. The grader is goodwe-session/scratch_s73/s73_nofault.py.
  • Recovery. Case a, A recovering into a house that fell below the parked target: worst grid +0 W with the reseed. Phase A measured -1059 W without the reseed and the site hold.

Review changes

  • The evidence must span 22 s, not only 12 cycles. At a 1 s meter the 12 cycles were 11 s, shorter than a healthy reading's lag (dead time plus a 10-17 s publish). The reviewer's harness (real Controller, both units with 6 s dead time and a 13-17 s publish, load steps 300/3000 W every 2 min) then refused both healthy units 10-16 times an hour, and each time the site held 0 W: +50-85 Wh import an hour and a +4.6 kW grid peak against +3.7 kW without the refusal. At a 2 s tick there were none. REFUSED_SPAN_S is (SETTLE_CYCLES - 1) * LOOP_TICK_S, so 2 s and slower ticks are unchanged.
  • The cut-out WARNING is for a refusal to give power only. A full battery that stops taking charge is always above its cut-out level, and the WARNING told the installer to check a setting that has nothing to do with it.

Not covered

  • A unit that gives a steady part of its ask (100 W or more) is a limit, as at N=1. SAFETY-42/43 still report it.
  • pack_permissions stays SoC-based, so the peak guard may size a shave with a refusing battery counted. The shortfall shows as unmet_w.
  • A wedged unit (HW-01) reading ~0 W is now shown as a refusal, not reported as not delivering. Its share still moves, which is the gain SAFETY-42 measured.