diff --git a/goodwe_controller/test_p1.py b/goodwe_controller/test_p1.py index b8016e5..96221f0 100644 --- a/goodwe_controller/test_p1.py +++ b/goodwe_controller/test_p1.py @@ -57,6 +57,25 @@ def raises(name, fn): fails.append(name) +def built(src): + """`src.build()`, with any escaping exception turned into a visible value. + + ⚠️ Legibility of a RED, not leniency. build() is contracted to return a bool + and to funnel every bad telegram through ingest.reject() - a guard that goes + missing (say the "is the net entity cached at all" one) makes it raise + instead. That still fails the suite, but by aborting it with a traceback at + whichever check happened to run first, which costs the next person ten + minutes deciding whether the suite is broken or the code is. Returning the + exception makes it compare unequal to True/False, so the NAMED check goes red + and says which rule died. + """ + try: + return src.build() + except Exception as err: # noqa: BLE001 - a raise here is itself the failure + print(f" build() raised {type(err).__name__}: {err}") + return err + + def sample(net_import, net_export=0.0, at=BASE, phases=1, pi=None, pe=None): return make_sample(SOURCE_HA, net_import, net_export, phases=phases, phase_import_w=pi, phase_export_w=pe, @@ -614,20 +633,20 @@ NET = {"net": "sensor.p1_meter_active_power", "phase_net": []} ing = P1Ingest(phases=1, max_age_s=30.0) sig = HaSignedSource(None, ing, NET, token="x") -check("nothing cached yet builds nothing", sig.build() is False and ing.last is None) +check("nothing cached yet builds nothing", built(sig) is False and ing.last is None) sig._absorb("sensor.p1_meter_active_power", "1000") check("one signed entity is a complete telegram on its own", - sig.build() is True and ing.net_w == 1000.0) + built(sig) is True and ing.net_w == 1000.0) check("the sample is tagged with its own transport", ing.last.source == SOURCE_HA_SIGNED) sig._absorb("sensor.p1_meter_active_power", "-2500") -sig.build() +built(sig) check("a negative state lands as a negative net", ing.net_w == -2500.0) before = ing.last sig._absorb("sensor.p1_meter_active_power", "unavailable") check("an unavailable signed entity is a parse error", ing.parse_errors == 1) -check("an unavailable entity does not build a sample", sig.build() is False) +check("an unavailable entity does not build a sample", built(sig) is False) # ⚠️ The rule the whole ticket turns on: a missing reading is MISSING. Resolving # it to 0 W would read as a perfectly balanced house and defeat the staleness # trigger that FW-01's watchdog is built on. @@ -640,7 +659,7 @@ check("a non-numeric signed state is a parse error, not 0 W", ing.parse_errors == 3 and ing.net_w == -2500.0) sig._absorb("sensor.p1_meter_active_power", "64954") check("64954 is refused at the signed transport too", - sig.build() is False and ing.parse_errors == 4) + built(sig) is False and ing.parse_errors == 4) sig._absorb("sensor.not_ours", "123") check("an unsubscribed entity is never cached by the signed transport", "sensor.not_ours" not in sig.cache) @@ -652,7 +671,7 @@ sig = HaSignedSource(None, ing, dict(NET), token="x") ing.submit(make_sample(SOURCE_HA_SIGNED, 1200, 0, phases=1, ingest_mono=time.monotonic() - 20.0)) sig._absorb("sensor.p1_meter_active_power", "unavailable") -sig.build() +built(sig) check("a rejected reading does not reset the published age", ing.published_age_s > 19 and ing.net_w == 1200.0) ing.submit(make_sample(SOURCE_HA_SIGNED, 1200, 0, phases=1, @@ -669,10 +688,10 @@ NET3 = {"net": "sensor.p1_meter_active_power", sig3 = HaSignedSource(None, ing3, NET3, token="x") for eid, val in (("sensor.p1_meter_active_power", "187"), ("sensor.p1_l1", "2301")): sig3._absorb(eid, val) -check("an incomplete signed phase set waits instead of guessing", sig3.build() is False) +check("an incomplete signed phase set waits instead of guessing", built(sig3) is False) sig3._absorb("sensor.p1_l2", "468") sig3._absorb("sensor.p1_l3", "-2582") -check("a complete signed three-phase set builds", sig3.build() is True) +check("a complete signed three-phase set builds", built(sig3) is True) check("signed per-phase entities keep the exporting phase negative", ing3.last.per_phase_w == (2301.0, 468.0, -2582.0)) check("per-phase IMPORT clamps the exporting phase to zero", @@ -688,7 +707,7 @@ sig_bad = HaSignedSource(None, P1Ingest(phases=3, max_age_s=30.0), for eid in ("sensor.net", "sensor.a", "sensor.b"): sig_bad._absorb(eid, "100") check("two phases delivered against meter_phases 3 is rejected, not padded", - sig_bad.build() is False and sig_bad.ingest.last is None + built(sig_bad) is False and sig_bad.ingest.last is None and sig_bad.ingest.parse_errors == 1) # --------------------------------------------------------------------------- #