SAFETY-73: a battery that refuses, per inverter, with two or more
SAFETY-52's hold applied per unit at N>1 (owner, 2026-09-27, option R). A unit that gives < 100 W for 12 cycles in one direction while asked > 200 W, or trips its stale-reading gate on a ~0 W reading, gets a P_SAFETY `refused` unit limit closing that direction, so the allocator hands its share to the others within their own limits. Probe: a +-200 W pin every 15 min, one unit at a time, only while the site asks 200 W that way. All units refusing: the site law holds 0 W. Exits reseed the site target from the wire. INFO plus one WARNING (REFUSES_ABOVE_CUT_OUT) above the configured cut-out; SAFETY-42/43 no longer report a refusing unit. N=1 unchanged. test_refusal_units.py: cases a/b/c on flat and jitter registers against the old behaviour, probes, recovery, exits; mutations U1-U13 all red. TEST-19 no-fault campaign: 0 engagements in 5280 runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
652097abde
commit
016ed5a5a3
@@ -33,7 +33,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
from named_absence import given, present
|
||||
|
||||
from app import messages
|
||||
from app import control, messages
|
||||
from app.control import Tuning
|
||||
from app.delivery import (
|
||||
DELIVERY_OPTION_DEFAULTS, DELIVERY_SHARE_FRACTION, REASON_CODE,
|
||||
@@ -592,7 +592,21 @@ check("...and the SHARES now differ, which is OPT-09 and not a weakening of the
|
||||
"cards meet here from opposite sides: OPT-09 stops the ASK, this file "
|
||||
"grades the ALARM, and neither can do the other's half",
|
||||
_pw != _pf
|
||||
and _sf[-1]["shares"][1] == 0.0 and _sw[-1]["shares"][1] == 2000.0)
|
||||
and _sf[-1]["shares"][1] == 0.0
|
||||
and max(r["shares"][1] for r in _sw) == 2000.0)
|
||||
# SAFETY-73. A unit that gives < 100 W for 12 cycles while asked > 500 W is a
|
||||
# battery that refuses (owner, 2026-09-27): it is closed, its share moves, and
|
||||
# SAFETY-42 is left with units that deliver something but not enough. This
|
||||
# fixture compresses `delivery_hold_s` to 1 s at 0.2 s cycles, so the report
|
||||
# still comes first here. On a site 185 s against 60 s puts the refusal first.
|
||||
_ref_at = next((i for i, r in enumerate(_sw) if r["shares"][1] == 0.0), None)
|
||||
check("SAFETY-73: the wedged arm's B, giving 0 W against 2000 W, is refused "
|
||||
"after 12 cycles of that evidence and asked 0 W from then, the report "
|
||||
"having come first only because this fixture's hold is 1 s",
|
||||
_ref_at is not None and _ref_at >= control.SETTLE_CYCLES
|
||||
and all(r["shares"][1] > 0 for r in _sw[:_ref_at])
|
||||
and all(r["shares"][1] == 0.0 for r in _sw[_ref_at:])
|
||||
and _cw.inverters[1].refused == 1)
|
||||
check("...and the state of charge is still what tells them apart",
|
||||
[r["soc"] for r in _sw] != [r["soc"] for r in _sf])
|
||||
print(f" both arms, last cycle: grid {_sw[-1]['grid']:.0f} W, shares "
|
||||
@@ -640,10 +654,10 @@ check("...and `account()` STILL turns this pack away if the ask does arrive - "
|
||||
account(live=True, parked=False, soc=_cold_b.soc,
|
||||
bms_floor=_cold_b.bms_soc_floor) is not None)
|
||||
check("...and the WEDGED arm's B differs from it in exactly that one place - "
|
||||
"which is why it is still asked for its share, and still named",
|
||||
"which is why it was asked for its share until the refusal, and named",
|
||||
account(live=True, parked=False, soc=_cw.inverters[1].soc,
|
||||
bms_floor=_cw.inverters[1].bms_soc_floor) is None
|
||||
and float(_cw.inverters[1].standing_w()) > 0)
|
||||
and _sw[0]["shares"][1] > 0 and _sw[-1]["named"] == ("inverter-b",))
|
||||
|
||||
# ⚠️ WHICH FLOOR, AND THIS IS A WIRING CHECK THAT THE PURE ONES ABOVE CANNOT
|
||||
# MAKE. `peakguard.pack_floor_pct()` conjoins a pack's own cut-out with the
|
||||
@@ -916,9 +930,13 @@ check("...and EVERY WATT IS THE SAME. Grid, site target, both shares and both "
|
||||
"reported or not - which is what 'it does not act' means, stated as a "
|
||||
"measurement instead of as an intention",
|
||||
_power_domain(_sw) == _power_domain(_sn))
|
||||
check("...the wedged unit is still being commanded its share after the trip, "
|
||||
"not parked: this is NOT SAFETY-02's remedy and must not become it",
|
||||
abs(float(_cw.inverters[1].target)) > 0)
|
||||
_trip_at = next((i for i, r in enumerate(_sw) if r["named"]), None)
|
||||
check("...the wedged unit is still commanded its share after the trip, not "
|
||||
"parked by this guard: this is NOT SAFETY-02's remedy. What later takes "
|
||||
"the share is SAFETY-73's refusal, which runs the same in the arm where "
|
||||
"this guard never trips (the line above)",
|
||||
_trip_at is not None and _ref_at is not None and _trip_at < _ref_at
|
||||
and all(r["shares"][1] > 0 for r in _sw[_trip_at:_ref_at]))
|
||||
check("...and nothing named `delivery` reaches the arbiter - the resolution's "
|
||||
"own sentence is unchanged by the trip",
|
||||
"deliver" not in _cw.reason.lower() and _cw.reason == _cn.reason)
|
||||
|
||||
Reference in New Issue
Block a user