#!/usr/bin/env python3 """OPT-11 P3: with battery modes off, is every command byte-identical to release/1.0 7a00f12, on the paths P3 changed? python3 tools/opt11p3_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) plus a day-ahead price series and `arbitrage_enabled`, so the site trade P3 now skips with modes on (`arbitrage.decide()`, its claim, the sizing to the guard's headroom) runs on every cycle, with a buy period, a sell period and periods in between, and a house that goes over the limit. Each row hashes safety79's fields plus the trade's action, watts and sentence. Uses only the API 7a00f12 has, so the same file runs on both trees; test_modes_p3.py part 1 holds this tree to the digests printed on 7a00f12. """ import functools import hashlib import os import re import sys import tempfile import time from datetime import timedelta 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 from app.p1 import make_sample, split_signed # noqa: E402 from app.price import PricePoint, PriceProduct, PriceRead, brussels # noqa from app.static_tariff import TariffPlan # noqa: E402 DUR = 5400.0 def prices(): """15-minute firm prices: the cheapest period 15-30 min into the run, the dearest 75-90 min in, and ones in between.""" pts = [] for i in range(-8, 40): s = rig.BASE + timedelta(minutes=15 * i) p = 0.08 if i == 1 else 0.52 if i == 5 else 0.20 + 0.003 * (i % 7) pts.append(PricePoint(start=brussels(s), eur_per_kwh=p, provisional=False)) return TariffPlan(PriceRead.series(pts, PriceProduct.DAY_AHEAD, "generic_dayahead", rig.BASE), None, "provider") PLAN = prices() def _feed(c, t): c.tariff_plan, c.tariff_at = PLAN, rig.BASE + timedelta(seconds=t) s79._feed(c, t) # OPT-15: the P1 sample of each cycle is built this long after the cycle # reads the grid (the HA source's debounce), so the cycle on a quarter # boundary still sees the old block, as on TEST-23. P1_LAG_S = 0.35 def _feed_lag(c, t): c.tariff_plan, c.tariff_at = PLAN, rig.BASE + timedelta(seconds=t) imp, exp = split_signed(c.hass.grid()) c.p1.submit(make_sample( "ha_signed", imp, exp, phases=1, ingest_ts=rig.BASE + timedelta(seconds=t - P1_LAG_S), ingest_mono=time.monotonic())) def load(t): """Under the limit, over it, a surplus, a big evening load.""" k = int(t // 600) % 5 return (900.0, 3200.0, -2200.0, 1500.0, 4200.0)[k] def soc(u, t): return 55.0 if u == "a" else 45.0 ARB = {"arbitrage_enabled": True, "cycle_cost_threshold_eur": 0.05} SCENARIOS = { "n1": dict(n=1), "n2": dict(n=2), # The option present and false: an install that saved it off. "n1_off": dict(n=1, extra={"battery_modes_enabled": False}), "n2_off": dict(n=2, extra={"battery_modes_enabled": False}), # OPT-15: every quarter boundary with the old block still open. "n1_lag": dict(n=1, lag=True), "n2_lag": dict(n=2, lag=True), } def run(n, *, extra=None, dur=DUR, lag=False): opts = dict(s79.P1, **ARB, **(extra or {})) trade = [] def on_cycle(c, t): a = c.arb trade.append(None if a is None else (a.action, a.power_w, a.reason)) db = os.path.join(tempfile.mkdtemp(prefix="p3id-"), "tariff.db") real = rig.Controller rig.Controller = functools.partial(real, peak_db_path=db) try: rows, c = rig.run(n, extra=opts, dur=dur, load=load, soc=soc, cut=lambda u, t: False, before=_feed_lag if lag else _feed, on_cycle=on_cycle) finally: rig.Controller = real return [r + (x,) for r, x in zip(rows, trade)], c def digest(rows) -> str: return hashlib.sha256(repr(rows).encode()).hexdigest() def _trade_words(text): """PRICE-02: the trade sentence as release/1.0 before it printed it. The plan now drops price periods that have ended, so a sale's "above the cheapest" figure is a different number once the first cheap period is over, and the sentences say "price period" where they said "hour". Both maps are applied to both sides, so only those two things are hidden. ONLY FOR COMPARING WITH A TREE OLDER THAN PRICE-02. A pin of this tree itself uses `digest()`, with nothing hidden.""" text = text.replace("this hour is neither the buy hour nor the sell hour", "now is not the cheapest or the dearest price period") text = re.sub(r"[0-9]+\.[0-9]+ above the cheapest", "N above the cheapest", text) return text.replace("firm hour at", "firm price period at") def trade_digest(rows) -> str: """`digest()` with the trade sentence passed through `_trade_words()`: every command, integrator and allocation, and the action, watts and the rest of the sentence, byte for byte.""" return digest([r[:-1] + (None if r[-1] is None else (r[-1][0], r[-1][1], _trade_words(r[-1][2])),) for r in rows]) 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] acts = sorted({str(r[-1][0]) if r[-1] else "None" for r in rows}) print(f"{k}: {len(rows)} cycles, trade actions {acts}") for k, v in digests().items(): print(f"{k} {v}")