"""SMA-2: the inverter driver seam. `python3 test_sma2_driver.py` Part 1 grades the GoodWe driver against a Home Assistant that logs every call: it must make exactly the calls the code it replaced made, in the same order. Part 2 grades what `Inverter` reads from its driver. Part 3 drives a REAL Controller with a roster of one fake-driver unit and one GoodWe unit and checks that it writes, reads, limits, grades, drops and publishes each by its own driver, with a Home Assistant that records every entity it is asked about so a call site that goes round the seam is seen. Part 4 is the SAFETY-28 census and rule 9 on the new keys. Part 5 is the schema. Mutating a module this file grades? Read MUTATION.md first (purge __pycache__). The seam mutants this file was run against are listed at the end. """ import asyncio import io import sys from datetime import datetime, timedelta, timezone from pathlib import Path from app import drivers from app.drivers import (DEFAULT_DRIVER, REGISTRY, SELECTABLE, Driver, DriverNotBound, GoodWeEsphomeDriver, Probe, UnavailableDriver, build_driver, census_key) from app.freshness import BOUNDS, Bound from app.inverter import LINK_STALE_S, Inverter, build_roster from app.main import Controller from app.store import DEFAULTS from app.validate import (actuator_writers, validate_startup_config, validate_unit_ranges) fails = [] HERE = Path(__file__).resolve().parent def check(name, cond): print(f" {'ok ' if cond else 'FAIL'} {name}") if not cond: fails.append(name) class MemStore: def __init__(self): self.data = dict(DEFAULTS) def set(self, k, v): self.data[k] = v def get_time(self, *a): return None def set_time(self, *a): pass class Pub: """Keeps the last published values, like test_mqtt.py's.""" enabled = False def __init__(self): self.last = {} def publish(self, values): self.last = dict(values) def close(self): pass class LogHass: """Home Assistant for the GoodWe unit. EVERY call is logged with its entity, so a path that talks to Home Assistant about anything else is on record.""" def __init__(self, numbers=None, limits=None, accept=True): self.numbers = dict(numbers or {}) self.limits_by = dict(limits or {}) self.accept = accept self.log = [] # (method, entity, extra) async def number(self, entity, invert=False): self.log.append(("number", entity, invert)) v = self.numbers.get(entity) return None if v is None else (-v if invert else v) async def limits(self, entity): self.log.append(("limits", entity, None)) return self.limits_by.get(entity, (None, None, None)) async def set_number(self, entity, value): self.log.append(("set_number", entity, value)) if self.accept: self.numbers[entity.replace("number.sp", "sensor.batt")] = value return self.accept async def state(self, entity): self.log.append(("state", entity, None)) return None async def call(self, *a): self.log.append(("call", repr(a), None)) return True def entities(self): """Every entity name Home Assistant was asked about (service calls like notifications name none).""" return {e for m, e, _x in self.log if m != "call"} def run(coro): return asyncio.run(coro) def raises(exc, make): """Does `run(make())` raise `exc`? `make` builds the coroutine.""" try: run(make()) except exc: return True except Exception: return False return False # --------------------------------------------------------------------------- # a fake driver: a unit reached by host:port:unit, no Home Assistant at all # --------------------------------------------------------------------------- FAKE_BOUND = Bound(signal="fake", unchanged_s=11.0, move_w=100.0, release_s=5.0, wired=True, derivation="test") class FakeDriver(Driver): name = "fake" writer_key = "host" brand = "Fake" link_stale_s = 7.0 batt_bound = FAKE_BOUND settle_s = 3.0 words = {"idle": "handed back"} def __init__(self, host="", port=502, unit_id=1, rating=1000.0, soc=60.0): self.host, self.port, self.unit_id = host, port, unit_id self.rating, self.soc, self.power = rating, soc, 0.0 self.accept = True self.calls = [] # (what, arg) @classmethod def from_entry(cls, entry): return cls(entry.get("host", ""), entry.get("port", 502), entry.get("unit_id", 1), rating=entry.get("_rating", 1000.0)) @classmethod def actuator_id_of(cls, entry): host = str(entry.get("host") or "").strip() return (f"{host}:{entry.get('port', 502)}:{entry.get('unit_id', 1)}" if host else "") def configured(self): return bool(self.host) def actuator_id(self): return f"{self.host}:{self.port}:{self.unit_id}" def soc_source(self): return "fake-soc" def batt_source(self): return "fake-power" async def limits(self): self.calls.append(("limits", None)) return -self.rating, self.rating async def read(self): self.calls.append(("read", None)) return self.soc, self.power, {"state": "ok"} async def write(self, w): self.calls.append(("write", w)) if self.accept: self.power = w # the battery follows its command return self.accept async def release(self): self.calls.append(("release", None)) return True async def probe(self): self.calls.append(("probe", None)) return Probe(-self.rating, self.rating, self.soc, self.power, {}) def calls(drv, what): return [a for w, a in drv.calls if w == what] print("SMA-2 - the inverter driver seam") print() # ============================================================================== print("1. the GoodWe driver makes the calls the code it replaced made") GW = dict(setpoint_entity="number.sp_a", soc_entity="sensor.soc_a", batt_entity="sensor.batt_a") h1 = LogHass({"sensor.soc_a": 55.0, "sensor.batt_a": 700.0}, {"number.sp_a": (-5000.0, 4000.0, 1.0)}) d1 = GoodWeEsphomeDriver(**GW).bind(h1) check("a driver built from an entry names its three entities and is configured", GoodWeEsphomeDriver.from_entry(dict(GW)).setpoint_entity == "number.sp_a" and d1.configured() and d1.actuator_id() == "number.sp_a" and (d1.soc_source(), d1.batt_source()) == ("sensor.soc_a", "sensor.batt_a")) check("...and one with no setpoint entity is not", not GoodWeEsphomeDriver().configured() and not GoodWeEsphomeDriver(soc_entity="sensor.soc_a").configured()) check("limits() is hass.limits(setpoint) and hands back (min, max)", run(d1.limits()) == (-5000.0, 4000.0) and h1.log == [("limits", "number.sp_a", None)]) h1.log.clear() check("read() asks for the SoC first, then the power, with the invert flag, " "and nothing else", run(d1.read()) == (55.0, 700.0, {}) and h1.log == [("number", "sensor.soc_a", False), ("number", "sensor.batt_a", False)]) h1.log.clear() d1i = GoodWeEsphomeDriver(batt_invert=True, **GW).bind(h1) check("...and a reversed power sensor is reversed by hass.number, as before", run(d1i.read()) == (55.0, -700.0, {}) and h1.log[1] == ("number", "sensor.batt_a", True)) h1.log.clear() check("write(w) is hass.set_number(setpoint, w) and says whether it took it", run(d1.write(-250.0)) is True and h1.log == [("set_number", "number.sp_a", -250.0)]) h1.log.clear() h1.accept = False check("...and False when Home Assistant refuses", run(d1.write(10.0)) is False) h1.accept = True h1.log.clear() check("release() is write(0): GoodWe is handed back by its board's own watchdog", run(d1.release()) is True and h1.log == [("set_number", "number.sp_a", 0.0)]) h1.log.clear() h1.numbers.update({"sensor.soc_a": 55.0, "sensor.batt_a": 700.0}) pr = run(d1.probe()) check("probe() reads the range, the SoC and the power and WRITES NOTHING", pr == Probe(-5000.0, 4000.0, 55.0, 700.0, {}) and all(m != "set_number" for m, _e, _x in h1.log)) check("a driver with no Home Assistant is loud, not a silent 'no answer'", raises(DriverNotBound, GoodWeEsphomeDriver(**GW).limits) and raises(DriverNotBound, GoodWeEsphomeDriver(**GW).read) and raises(DriverNotBound, lambda: GoodWeEsphomeDriver(**GW).write(1.0)) and not GoodWeEsphomeDriver(**GW).bound()) _swap = {"h": h1} d1g = GoodWeEsphomeDriver(**GW).bind_getter(lambda: _swap["h"]) h1b = LogHass({"sensor.soc_a": 1.0, "sensor.batt_a": 2.0}) _swap["h"] = h1b check("bind_getter() looks Home Assistant up at each use, so a Controller whose " ".hass is replaced afterwards is followed", d1g.bound() and run(d1g.read()) == (1.0, 2.0, {}) and h1b.log) check("its numbers are the ones the code it replaced used", (d1.link_stale_s, d1.batt_bound, d1.brand) == (LINK_STALE_S, BOUNDS["batt"], "GoodWe") and LINK_STALE_S == 30.0 and d1.settle_s == 18.0 and d1.words == {}) # ============================================================================== print() print("2. what Inverter reads from its driver") NOW = datetime(2026, 11, 7, 12, 0, tzinfo=timezone.utc) fk = FakeDriver(host="10.9.9.9", unit_id=3) inv_f = Inverter("fake-a", driver=fk, max_w=2000.0) inv_g = Inverter("inverter-b", max_w=3000.0, **{ "setpoint_entity": "number.sp_b", "soc_entity": "sensor.soc_b", "batt_entity": "sensor.batt_b"}) check("an Inverter built from the old keywords gets the GoodWe driver, with " "the same entities on the same attributes", isinstance(inv_g.driver, GoodWeEsphomeDriver) and (inv_g.setpoint_entity, inv_g.soc_entity, inv_g.batt_entity, inv_g.batt_invert) == ("number.sp_b", "sensor.soc_b", "sensor.batt_b", False)) check("...and one given another driver has none of those entities", (inv_f.setpoint_entity, inv_f.soc_entity, inv_f.batt_entity) == ("", "", "")) check("has_writer() is the driver's configured()", inv_f.has_writer() and inv_g.has_writer() and not Inverter("x", driver=FakeDriver(host="")).has_writer() and not Inverter("y").has_writer()) check("the freshness gate is built on the DRIVER's bound", inv_f.batt_fresh.bound is FAKE_BOUND and inv_g.batt_fresh.bound is BOUNDS["batt"]) for u in (inv_f, inv_g): u.note_write(False, NOW - timedelta(seconds=10)) check("is_live() takes its window from the driver: 10 s of rejected writes " "drops a 7 s unit and not a GoodWe one", inv_f.is_live(NOW)[0] is False and inv_g.is_live(NOW)[0] is True) check("...and an explicit window still wins", inv_f.is_live(NOW, stale_s=60.0)[0] is True and inv_g.is_live(NOW, stale_s=5.0)[0] is False) inv_f5 = Inverter("fake-5", driver=FakeDriver(host="h"), max_w=2000.0) inv_f5.note_write(False, NOW - timedelta(seconds=5)) check("link_note() says the driver's window: 'after 7 seconds' for the fake " "unit, 'after 30 seconds' for the GoodWe one, and nothing for a healthy unit", "after 7 seconds" in inv_f5.link_note(NOW) and "after 30 seconds" in inv_g.link_note(NOW) and "cannot be controlled" in inv_f.link_note(NOW) and Inverter("z", driver=FakeDriver(host="h")).link_note(NOW) == "") check("an unconfigured unit gets the no-setpoint row, as before", Inverter("x", driver=FakeDriver(host="")).link_note(NOW).startswith("x: ") and Inverter("x", driver=FakeDriver(host="")).link_note(NOW) == Inverter("x").link_note(NOW) != "") check("an entry that names a driver this build lacks is an unconfigured unit, " "not a crash and not GoodWe", isinstance(build_driver({"driver": "nope", "setpoint_entity": "number.x"}), UnavailableDriver) and not build_driver({"driver": "nope"}).configured() and build_roster({"inverters": [{"name": "n", "driver": "nope", "setpoint_entity": "number.x", "max_w": 1000}]}, None)[0].has_writer() is False) # ============================================================================== print() print("3. a roster of one fake unit and one GoodWe unit, in a real Controller") REGISTRY["fake"] = FakeDriver OPTS = dict(meter_source="off", meter_entity="sensor.grid", max_w=5000, gain=0.6, slew_w=0, deadband_w=15, target_grid_w=0, step_w=10, heartbeat_s=10, stale_input_s=15, shadow_mode="off", inverters=[ dict(name="fake-a", driver="fake", host="10.9.9.9", port=502, unit_id=3, max_w=2000, bms_soc_floor=10), dict(name="inverter-b", setpoint_entity="number.sp_b", soc_entity="sensor.soc_b", batt_entity="sensor.batt_b", max_w=3000, bms_soc_floor=15)]) B_ENT = {"number.sp_b", "sensor.soc_b", "sensor.batt_b"} def site(rating_f=1000.0, **over): hass = LogHass({"sensor.grid": 3000.0, "sensor.soc_b": 50.0, "sensor.batt_b": 0.0}, {"number.sp_b": (-3000.0, 3000.0, 1.0)}) pub = Pub() c = Controller(dict(OPTS, **over), hass, MemStore(), pub, stop=asyncio.Event()) f = c.inverters[0].driver f.rating = rating_f c.inverters[0].dev_min, c.inverters[0].dev_max = -rating_f, rating_f c.inverters[1].dev_min, c.inverters[1].dev_max = -3000.0, 3000.0 c.auto = True return c, hass, pub, f async def cycles(c, n=6): for _ in range(n): await c.read_inputs() await c.cycle() c3, h3, p3, f3 = site() check("the roster is built through the registry: a fake unit and a GoodWe unit", isinstance(c3.inverters[0].driver, FakeDriver) and isinstance(c3.inverters[1].driver, GoodWeEsphomeDriver) and f3.actuator_id() == "10.9.9.9:502:3") run(cycles(c3)) fa, fb = c3.inverters print(f" after 6 cycles: fake-a wrote {calls(f3, 'write')}, GoodWe wrote " f"{[x for m, e, x in h3.log if m == 'set_number']}, shares " f"{c3.allocation.shares if c3.allocation else None}") check("READ: the unit's SoC and power come from its driver, the GoodWe unit's " "from its entities", calls(f3, "read") and fa.soc == 60.0 and fb.soc == 50.0 and ("number", "sensor.soc_b", False) in h3.log) check("WRITE: the fake unit's share went to ITS driver and the GoodWe unit's to " "ITS entity, each non-zero, the same direction", calls(f3, "write") and calls(f3, "write")[-1] > 0 and [x for m, e, x in h3.log if m == "set_number" and e == "number.sp_b"][-1] > 0) check("ALLOCATE: each unit holds a share inside its own rating, and the shares " "are the site's (3000 W of grid, fake capped at 1000 W by its driver)", c3.allocation is not None and set(c3.allocation.shares) == {"fake-a", "inverter-b"} and 0 < c3.allocation.shares["fake-a"] <= 1000.0 and 0 < c3.allocation.shares["inverter-b"] <= 3000.0 and calls(f3, "write")[-1] == c3.allocation.shares["fake-a"]) check("GRADE: the fake unit's rating is the driver's range (1000 W), the " "GoodWe unit's is its entry's (3000 W), each from its own source", (fa.rating_w(), fb.rating_w()) == (1000.0, 3000.0)) check("the fake's power follows its command, so the freshness gate (the fake's " "own bound) lets the reading through", fa.batt == calls(f3, "write")[-1] and fa.batt_fresh.bound is FAKE_BOUND) run(c3.send_heartbeats(10_000.0, 10.0)) n_before = len(calls(f3, "write")) check("HEARTBEAT: it re-emits the fake unit's standing command through its driver", n_before >= 2 and calls(f3, "write")[-1] == calls(f3, "write")[-2]) run(c3.command_site_zero()) check("STOP: command_site_zero() reaches the fake unit through its driver, as " "0 W (the hand-back is SMA-10's, and release() has no caller yet)", calls(f3, "write")[-1] == 0.0 and not calls(f3, "release")) c3.publish() print(f" published unit rows: " f"{sorted(k for k in p3.last if k.startswith('unit/'))}") check("PUBLISH: one set of rows per unit, with that unit's own numbers", p3.last.get("unit/fake_a/soc") == 60.0 and p3.last.get("unit/inverter_b/soc") == 50.0 and "unit/fake_a/battery" in p3.last and "unit/inverter_b/battery" in p3.last) rows = c3.checks() check("STATUS: the fake unit's sensor rows are not 'unset' (its driver has a " "source), and it has the control row", not [r for r in rows if r.get("id") == "sensor" and not r["ok"] and "fake" in r["text"]]) # liveness: the fake's 7 s window, the GoodWe unit's 30 s c4, h4, p4, f4 = site() run(cycles(c4, 2)) f4.accept = False # the fake's link is down; GoodWe's is fine _now = datetime.now(timezone.utc) for u in c4.inverters: u.note_write(False, _now - timedelta(seconds=10)) check("LIVENESS: after 10 s of rejected writes the fake unit is left out of the " "split and the GoodWe unit stays in it, per its driver's window", c4.live_units() == ("inverter-b",)) run(cycles(c4, 4)) check("...and the survivor carries the site: the allocation names only it", c4.allocation is not None and set(c4.allocation.shares) == {"inverter-b"} and c4.allocation.shares["inverter-b"] > 1000.0) # the tripwire: nothing in the world was asked about the fake unit asked = h3.entities() | h4.entities() print(f" Home Assistant was asked about: {sorted(asked)}") check("TRIPWIRE: Home Assistant was only ever asked about the GoodWe unit's " "entities and the meter - no empty name, no host - on read, write, " "heartbeat, stop and limits alike", asked <= B_ENT | {"sensor.grid"} and "" not in asked and not any("10.9.9.9" in e or "fake" in e for e in asked)) check("...and the GoodWe unit was never touched through the fake", not [1 for w, a in f3.calls + f4.calls if isinstance(a, str) and "number.sp_b" in a]) # limits and the per-unit range check, through run_control() async def drive(c, secs): try: await asyncio.wait_for(c.run_control(), timeout=secs) except asyncio.TimeoutError: pass c5, h5, p5, f5 = site(rating_f=2500.0) f5.rating = 2500.0 c5.inverters[0].dev_min = c5.inverters[0].dev_max = None c5.inverters[1].dev_min = c5.inverters[1].dev_max = None run(drive(c5, 3.0)) check("LIMITS: run_control() reads the fake unit's range from its driver and " "the GoodWe unit's from its entity", calls(f5, "limits") and (c5.inverters[0].dev_min, c5.inverters[0].dev_max) == (-2500.0, 2500.0) and (c5.inverters[1].dev_min, c5.inverters[1].dev_max) == (-3000.0, 3000.0) and [e for m, e, x in h5.log if m == "limits"] == ["number.sp_b"]) c6, h6, p6, f6 = site() f6.rating = 1500.0 # the entry says 2000 W c6.inverters[0].dev_min = c6.inverters[0].dev_max = None run(drive(c6, 10.0)) check("RULE 2 PER UNIT: a fake unit rated above what its driver says it accepts " "is refused by name, as a GoodWe one is", c6.refused and any("fake-a" in p and "10.9.9.9:502:3" in p for p in c6.refused) and not calls(f6, "write")) # the settings panel has no Controller: it binds each unit's driver itself from app import selfconfig # noqa: E402 hs = LogHass({}, {"number.sp_b": (-3000.0, 3000.0, 1.0)}) _ps, _unread = run(selfconfig.problems_and_ranges( dict(OPTS, inverters=[dict(OPTS["inverters"][0], _rating=1500.0), OPTS["inverters"][1]]), hs)) check("PANEL: the settings panel reads the fake unit's range from its driver " "and the GoodWe unit's from its entity, and grades the fake unit by name", any("fake-a" in p and "10.9.9.9:502:3 accepts at most 1500" in p for p in _ps) and [e for m, e, x in hs.log if m == "limits"] == ["number.sp_b"] and _unread == []) # the seam is the only way in: a fake-only roster touches Home Assistant for # nothing but the meter hz = LogHass({"sensor.grid": 800.0}) cz = Controller(dict(OPTS, inverters=[OPTS["inverters"][0]]), hz, MemStore(), Pub(), stop=asyncio.Event()) cz.inverters[0].dev_min, cz.inverters[0].dev_max = -1000.0, 1000.0 cz.auto = True run(cycles(cz, 3)) check("A FAKE-ONLY ROSTER never asks Home Assistant for anything but the " "meter, and still reads and writes (nothing on the control path is " "keyed on a GoodWe entity)", hz.entities() <= {"sensor.grid"} and calls(cz.inverters[0].driver, "read") and calls(cz.inverters[0].driver, "write")) # ============================================================================== print() print("4. SAFETY-28's census counts actuator ids; rule 9 on the new keys") def entry(**kw): return dict(name="u", max_w=3000, bms_soc_floor=10, **kw) check("GoodWe entries are counted as before: one entity named twice is one " "actuator with two writers, whatever its case", {k: len(v) for k, v in actuator_writers({"inverters": [ entry(setpoint_entity="number.X"), entry(setpoint_entity=" number.x ")]}).items()} == {"number.x": 2} and actuator_writers({"setpoint_entity": "number.a", "inverters": [ entry(setpoint_entity="number.a")]})["number.a"] == ["setpoint_entity", "inverters[0].setpoint_entity"]) cen = actuator_writers({"inverters": [ entry(driver="fake", host="gw", port=502, unit_id=1), entry(driver="fake", host="gw", port=502, unit_id=2), entry(driver="fake", host="gw", port=503, unit_id=1), entry(driver="fake", host="GW", port=502, unit_id=1)]}) print(f" census {cen}") check("another driver's actuator is host:port:unit: two units behind one " "gateway are two actuators, and one unit named twice is one", set(cen) == {"gw:502:1", "gw:502:2", "gw:503:1"} and cen["gw:502:1"] == ["inverters[0].host", "inverters[3].host"]) check("a GoodWe key on a fake entry declares no writer there, and a `host` on " "a GoodWe entry declares none", actuator_writers({"inverters": [entry(driver="fake", host="gw", setpoint_entity="number.x")]}) == {"gw:502:1": ["inverters[0].host"]} and actuator_writers({"inverters": [entry(setpoint_entity="number.x", host="gw")]}) == {"number.x": ["inverters[0].setpoint_entity"]}) check("an entry naming a driver this build lacks still counts its setpoint " "entity - refusing too much is the safe direction", actuator_writers({"inverters": [entry(driver="nope", setpoint_entity="number.q")]}) == {"number.q": ["inverters[0].setpoint_entity"]}) check("census_key() folds case and space", census_key(" Number.X ") == "number.x") GOOD = dict(entry(setpoint_entity="number.sp_a", soc_entity="sensor.soc_a", batt_entity="sensor.batt_a")) BASEOPTS = {"meter_source": "off", "meter_entity": "sensor.grid"} base_problems = validate_startup_config(dict(BASEOPTS, inverters=[dict(GOOD)])) print(f" baseline problems for a plain GoodWe roster: {base_problems}") def prob(**kw): return validate_startup_config(dict(BASEOPTS, inverters=[dict(GOOD, **kw)])) check("an existing entry (no new key) is graded exactly as before", prob() == base_problems) check("...and so is one that says driver goodwe_esphome, or carries every new " "key in a valid shape", prob(driver="goodwe_esphome") == base_problems and prob(driver="", host="", maintenance=False) == base_problems and prob(host="inv.local", port=502, unit_id=3, maintenance=True) == base_problems) def named(problems, key): return [p for p in problems if f"inverters.{key}" in p or f"({key})" in p] check("only goodwe_esphome can be selected for now", named(prob(driver="sma_modbus"), "driver") and named(prob(driver="fake"), "driver") and SELECTABLE == (DEFAULT_DRIVER,)) check("a host with a space, a bad port or unit number and a non-boolean " "maintenance are each refused by key; the right shapes are not", named(prob(host="a b"), "host") and named(prob(port=0), "port") and named(prob(port=70000), "port") and named(prob(port="x"), "port") and named(prob(port=True), "port") and named(prob(unit_id=248), "unit_id") and named(prob(unit_id=1.5), "unit_id") and named(prob(maintenance="yes"), "maintenance") and not named(prob(port=65535, unit_id=0, host="10.0.0.1"), "port")) check("a GoodWe entry with no setpoint entity is refused with the same sentence " "as before", any("has no" in p and "setpoint entity" in p.lower() for p in validate_startup_config(dict(BASEOPTS, inverters=[ dict(GOOD, setpoint_entity="")])))) fu = FakeDriver(host="gw") check("rule 2 grades any unit by its driver's actuator id, so a unit with no " "setpoint entity is no longer skipped", validate_unit_ranges([Inverter("a", driver=fu, max_w=2000.0)]) == [] and validate_unit_ranges([_u := Inverter("a", driver=fu, max_w=2000.0)]) == [] and (setattr(_u, "dev_max", 1000.0) or True) and len(validate_unit_ranges([_u])) == 1 and "gw:502:1" in validate_unit_ranges([_u])[0]) # ============================================================================== print() print("5. the schema") cfg = io.open(HERE / "config.yaml", encoding="utf-8").read() sch = cfg[cfg.index("\nschema:"):] ent = sch[sch.index("\n inverters:"):] ent = ent[:ent.index("\n\n")] decl = dict(ln.strip().lstrip("- ").split(": ", 1) for ln in ent.splitlines()[1:] if ": " in ln) check("setpoint_entity is optional per entry, beside driver, host, port, " "unit_id and maintenance; name, max_w and bms_soc_floor stay required", decl["setpoint_entity"] == "str?" and decl["driver"] == "list(goodwe_esphome)?" and decl["host"] == "str?" and decl["port"] == "port?" and decl["unit_id"] == "int(0,247)?" and decl["maintenance"] == "bool?" and not any(decl[k].endswith("?") for k in ("name", "max_w", "bms_soc_floor"))) check("config.yaml lists exactly the selectable drivers", decl["driver"] == f"list({'|'.join(SELECTABLE)})?") del REGISTRY["fake"] # THE SEAM MUTANTS THIS FILE WAS RUN AGAINST (twelve, each one edit to a copy of # the tree; all twelve turn it red): # write_unit calls hass.set_number itself -> part 3 WRITE (and the tripwire) # _read_unit calls hass.number(soc/batt entity) -> part 3 READ # run_control's limits keyed on setpoint_entity -> part 3 LIMITS # the panel's range check keyed on setpoint_entity -> part 3 PANEL # has_writer() back to bool(setpoint_entity) -> part 2 has_writer # is_live() window back to LINK_STALE_S -> part 2 is_live # link_note() window back to LINK_STALE_S -> part 2 link_note # Freshness built on BOUNDS["batt"] -> part 2 freshness bound # _write_unit's guard keyed on setpoint_entity -> part 3 WRITE # the census back to the GoodWe key only -> part 4 census # rule 2 per unit back to .setpoint_entity -> part 3 RULE 2 PER UNIT # the status rows' source test back to the entities -> part 3 STATUS # NOT COVERED: the call in amain() that reads the flat setpoint entity's range # before any Controller exists. It runs only inside amain(), which no check # starts; it is two lines and the GoodWe driver's own limits(), graded in part 1. print() if fails: print(f"{len(fails)} FAILED") sys.exit(1) print("all passed")