At N>=2 a battery at or under its own peak reserve is held: it gives nothing for the peak (weight 0 while the site gives) and the guard's need is placed on the live, non-refusing batteries above theirs. The guard is floor-limited, and alarms, only when those cannot hold the limit at full power for ALARM_HOLD_S. The peak alarms no longer read "Not ready" or hold the Home page at 0 W, because control keeps running. N=1 is byte-identical. DECISIONS/SAFETY-87-guard-uses-able-batteries.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""SAFETY-87: at one inverter, is the peak guard byte-identical to before?
|
|
|
|
python3 tools/safety87_identity.py # prints one digest per scenario
|
|
|
|
safety84_identity's harness (the REAL Controller on a P1 meter, every claim
|
|
and the guard's decision hashed) with the battery under, crossing and above
|
|
the peak reserve, the house over the aim, in a maintenance charge and HOLD,
|
|
and under a month peak banked at 4000 W. Uses only the API release/1.0
|
|
b8ddfd7 has, so the same file runs on both trees; test_safety87.py holds this
|
|
tree to the digests printed there.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from datetime import timedelta
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
import opt11_identity as rig # noqa: E402
|
|
import safety84_identity as s84 # noqa: E402
|
|
|
|
DUR = 1800.0
|
|
|
|
|
|
def crossing(u, t):
|
|
"""30 % falling 1 % a minute to 12 %: crosses the 20 % reserve."""
|
|
return max(12.0, 30.0 - t / 60.0)
|
|
|
|
|
|
def store(phase=None):
|
|
"""In `phase` from the first cycle, or idle with a cycle done yesterday."""
|
|
s = rig._Store()
|
|
if phase is None:
|
|
s.data["last_completed"] = (rig.BASE - timedelta(days=1)).isoformat()
|
|
else:
|
|
s.data["phase"], s.data["phase_started"] = phase, rig.BASE.isoformat()
|
|
return s
|
|
|
|
|
|
SCENARIOS = {
|
|
"n1_under": dict(soc=lambda u, t: 15.0, load=lambda t: 5500.0),
|
|
"n1_crossing": dict(soc=crossing, load=lambda t: 5000.0),
|
|
"n1_above": dict(soc=lambda u, t: 60.0, load=lambda t: 5500.0),
|
|
"n1_under_banked": dict(soc=lambda u, t: 15.0, load=lambda t: 6000.0,
|
|
month_peak=4000.0),
|
|
"n1_charge_low": dict(soc=lambda u, t: 11.0, load=lambda t: 5000.0,
|
|
store="charge"),
|
|
"n1_hold": dict(soc=lambda u, t: 99.0, load=lambda t: 5500.0,
|
|
store="hold", cheap=False,
|
|
extra={"maintenance_hold_min": 600}),
|
|
}
|
|
|
|
|
|
def digests() -> dict:
|
|
out = {}
|
|
for k, v in SCENARIOS.items():
|
|
v = dict(v)
|
|
st = store(v.pop("store", None))
|
|
out[k] = s84.digest(s84.run(1, dur=DUR, store=st, **v)[0])
|
|
return out
|
|
|
|
|
|
if __name__ == "__main__":
|
|
for k, v in digests().items():
|
|
print(f"{k} {v}")
|