#!/usr/bin/env python3 """SAFETY-78: at one inverter, is a maintenance drain byte-identical to before? python3 tools/safety78_identity.py # prints one digest per scenario tools/safety79_identity.py's harness (the REAL Controller on a P1 meter, so the peak guard runs) with the battery draining for its monthly cycle from the first cycle, under a house below, at and above the drain rate, a solar surplus and a load over the peak limit. Each row hashes safety79's fields, the modes' roles and the trade, the action log's newest line, and every claim main.py hands `resolve()`, so a claim or a log line added at N=1 that changes no watt still moves the digest. The `_modes` scenarios run battery modes on, Cheap hours only, over opt11p3_identity's prices. Uses only the API release/1.0 5a4cdc2 has, so the same file runs on both trees; test_safety78.py part 5 holds this tree to the digests printed there. """ import functools import hashlib import os import sys import tempfile sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import opt11_identity as rig # noqa: E402 import opt11p3_identity as p3 # noqa: E402 import safety79_identity as s79 # noqa: E402 import app.main as m # noqa: E402 from app import packmode # noqa: E402 from app.store import unit_key # noqa: E402 DUR = 2400.0 DRAIN = {"maintenance_enabled": True, "maintenance_start_hour": 11} def load(t): """Below the 2000 W drain, at it, above it, a surplus, over the limit.""" k = int(t // 480) return (1200.0, 2000.0, 3000.0, -1500.0, 6000.0)[min(k, 4)] def store_cheap(n): s = rig._Store() for u in ("inverter-a", "inverter-b")[:n]: s.data[unit_key(u, packmode.MODE_KEY)] = packmode.CHEAP_HOURS return s SCENARIOS = { "n1_drain": dict(n=1), "n1_drain_modes": dict(n=1, modes=True), } def run(n, *, modes=False, dur=DUR): extra = dict(s79.P1, **DRAIN) if modes: extra.update(p3.ARB, battery_modes_enabled=True) seen, extra_rows = [], [] real_resolve = m.resolve def spy(claims, **kw): seen.append(tuple((c.source, c.priority, c.kind, c.value, c.lo, c.hi, c.reason, c.actuator) for c in claims)) return real_resolve(claims, **kw) def on_cycle(c, t): g, tick, a = c.peak_guard, c.mode_tick, c.arb extra_rows.append(( seen[-1] if seen else None, None if g is None else (g.engaged, g.floor_w, g.reason), None if tick is None else (tuple(sorted(tick.roles.items())), tick.action, tick.why), None if a is None else (a.action, a.power_w, a.reason), [mm.phase for mm in c.maints], len(c.events), c.events[0] if c.events else None)) db = os.path.join(tempfile.mkdtemp(prefix="s78-"), "tariff.db") real = rig.Controller rig.Controller = functools.partial(real, peak_db_path=db) m.resolve = spy try: rows, c = rig.run(n, extra=extra, dur=dur, load=load, soc=s79.soc, cut=lambda u, t: False, before=p3._feed if modes else s79._feed, on_cycle=on_cycle, store=store_cheap(n) if modes else None) finally: rig.Controller = real m.resolve = real_resolve return [r + (x,) for r, x in zip(rows, extra_rows)], c def digest(rows) -> str: return hashlib.sha256(repr(rows).encode()).hexdigest() def digests() -> dict: return {k: digest(run(**v)[0]) for k, v in SCENARIOS.items()} if __name__ == "__main__": if "--summary" in sys.argv: for k, v in SCENARIOS.items(): rows = run(**v)[0] print(f"{k}: {len(rows)} cycles, phases " f"{sorted({str(r[-1][-1]) for r in rows})}, targets " f"{sorted({r[1] for r in rows})[:6]}") for k, v in digests().items(): print(f"{k} {v}")