#!/usr/bin/env python3 """SAFETY-84: at one inverter, is a maintenance charge byte-identical to before? python3 tools/safety84_identity.py # prints one digest per scenario safety78_identity's harness (the REAL Controller on a P1 meter, every claim hashed) with the battery in its maintenance CHARGE phase from the first cycle, on the owner's settings (limit 3500, margin 50, charge 2000 W). The house steps below the charge rate, at it, under the aim, over it and into a solar surplus, in a cheap window, outside one, and under a month peak banked at 4000 W. Uses only the API release/1.0 9d5b7d1 has, so the same file runs on both trees; test_safety84.py holds this tree to the digests printed there (N=1) and to the new N=2 ones. """ 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 safety79_identity as s79 # noqa: E402 import app.main as m # noqa: E402 from app import tariff # noqa: E402 DUR = 2400.0 OPT = {"peak_cap_w": 3500, "peak_margin_w": 50, "maintenance_enabled": True, "maintenance_start_hour": 11, "maintenance_charge_w": 2000, "price_now_entity": "sensor.pnow", "price_avg_entity": "sensor.pavg"} def load(t): """Below the 2000 W charge, at it, under the aim, over it, a surplus.""" return (800.0, 2000.0, 3000.0, 5000.0, -1500.0)[min(int(t // 480), 4)] def feed(cheap): """s79's P1 feed plus a price now under (cheap) or over the average.""" now = 0.05 if cheap else 0.30 def before(c, t): if not getattr(c.hass, "_s84", False): read = c.hass.number async def number(e, invert=False): if e == "sensor.pnow": return now if e == "sensor.pavg": return 0.20 return await read(e, invert) c.hass.number, c.hass._s84 = number, True s79._feed(c, t) return before SCENARIOS = { "n1_charge": dict(n=1), "n1_charge_dear": dict(n=1, cheap=False), "n1_charge_banked": dict(n=1, month_peak=4000.0), "n2_charge": dict(n=2), "n2_charge_dear": dict(n=2, cheap=False), "n2_charge_banked": dict(n=2, month_peak=4000.0), } def run(n, *, cheap=True, month_peak=None, dur=DUR, load=load, soc=None, before=None, cut=None, extra=None, store=None): """Rows (opt11_identity's fields plus every claim, the guard and the phases) and the controller. `month_peak` banks that peak this month.""" 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 = c.peak_guard extra_rows.append(( seen[-1] if seen else None, None if g is None else (g.engaged, g.floor_w, g.reason), c._maint_shape_suspended, [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="s84-"), "tariff.db") if month_peak is not None: tariff.PeakStore(db)._db.execute( "INSERT INTO monthly_peak VALUES (?, ?, ?, NULL)", (tariff.month_key(rig.BASE), month_peak, "2026-09-02T10:00:00+00:00")).connection.commit() real = rig.Controller rig.Controller = functools.partial(real, peak_db_path=db) m.resolve = spy try: rows, c = rig.run(n, extra=dict(s79.P1, **OPT, **(extra or {})), dur=dur, load=load, soc=soc or s79.soc, cut=cut or (lambda u, t: False), before=before or feed(cheap), on_cycle=on_cycle, store=store or s79.store_charging(n)) 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__": for k, v in digests().items(): print(f"{k} {v}")