From 313503724d7ad84469ace48511e93e7c5cb41548 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sat, 7 Nov 2026 21:31:28 +0100 Subject: [PATCH] SMA-10 round 3: Start clears a failed hand-back streak; the hook is tested When control takes the unit again, release_fails, release_retry_at and release_zero_sent are reset, so the next Stop is a new streak with its own one 0 W, error line and event. Tests: a streak that Start interrupts without a recovery; a driver that overrides release_fallback(). _BACK_NL "de eigen sturing" (round 2) is kept. Co-Authored-By: Claude --- goodwe_controller/app/main.py | 8 ++- .../test_sma10_two_idle_states.py | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index afeb1a3..6b2e6fe 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -4162,9 +4162,15 @@ class Controller: if self.stopping or self.shadow or not self.auto: await self._hand_back(inv) return - if inv.released: + if inv.released or inv.release_fails or inv.release_zero_sent: + # Control has the unit again: forget the hand-back, and any + # failed streak with it, so the next Stop starts clean (its own + # one 0 W, its own error line and event). inv.released = False inv.last_write = None + inv.release_fails = 0 + inv.release_retry_at = 0.0 + inv.release_zero_sent = False entity = inv.driver.actuator_id() # for the log line below # --- SAFETY-03: THE CLAMP, AND IT IS ON THE ONLY PATH TO THE WIRE ----- diff --git a/goodwe_controller/test_sma10_two_idle_states.py b/goodwe_controller/test_sma10_two_idle_states.py index 2d4d614..4990941 100644 --- a/goodwe_controller/test_sma10_two_idle_states.py +++ b/goodwe_controller/test_sma10_two_idle_states.py @@ -1004,6 +1004,58 @@ check("SHUTDOWN with a failed release: the one 0 W is sent, inside the bound", check("a driver's own fallback replaces the 0 W (the hook), not the add-on", "release_fallback" in dir(HB) and HB.release_fallback is drivers.Driver.release_fallback) + +# a failed streak that Start interrupts WITHOUT a recovery: the next Stop is a +# new streak with its own one 0 W, its own error line and its own event +c, h, f = failing_site("false") +_cap.clear() +run(c.handle_action("auto_toggle")) # Stop, release fails +w1 = writes_of(f).count(0.0) +run(c.handle_action("auto_toggle")) # Start, no recovery +iv = c.inverters[1] +run(cycles(c, 5)) +nz = [w for w in writes_of(f) if w] +check("START after a failed release: control writes non-zero again and the " + "failed-streak state is cleared", + nz and writes_of(f)[-1] != 0.0 and iv.release_fails == 0 + and not iv.release_zero_sent and iv.release_retry_at == 0.0) +n_before = len(writes_of(f)) +run(c.handle_action("auto_toggle")) # Stop, release fails again +after = writes_of(f)[n_before:] +errs = [m for lv, m in _cap if lv >= logging.ERROR and "inverter-b" in m] +evs = [e for e in c.events if "hand-back failed" in str(e)] +check("SECOND STREAK (Start came between, no recovery): the unit gets its own " + "one 0 W write", + after == [0.0] and w1 == 1) +check("SECOND STREAK: a second error line and a second action-log event", + len(errs) == 2 and len(evs) == 2) +run(c.send_heartbeats(70_000.0, 10.0)) +check("...and still only the one 0 W in that streak", + writes_of(f)[n_before:] == [0.0]) + +# the hook: a driver's own fallback replaces the 0 W, and _hand_back does not +# call write(0) itself +class HookHB(HB): + name = "hookhb" + + async def release_fallback(self): + self.calls.append(("fallback", None)) + return await self.write(5.0) # a non-zero safe value + + +REGISTRY["hookhb"] = HookHB +c, h, p = site([gw("inverter-a", "a"), fk("inverter-b", "hookhb")]) +f = fakes(c)[0] +run(cycles(c, 4)) +f.mode = "false" +n0 = len(f.calls) +run(c.handle_action("auto_toggle")) +tail = [x for x in f.calls[n0:] if x[0] in ("release", "fallback", "write")] +print(f" a driver with its own fallback: {tail}") +check("HOOK: after a failed release the driver's release_fallback() is called " + "once, its own value (5 W) is the only write, and the add-on does not " + "write 0 W itself", + tail == [("release", None), ("fallback", None), ("write", 5.0)]) logging.getLogger("goodwe").removeHandler(_h2) # --- 3i. per-unit state and the other counts --------------------------------- @@ -1149,3 +1201,5 @@ print("all passed") # o. (round 2) a failed release is retried on every idle write. # p. (round 2) every failed try is logged. # q. (round 2) no fallback 0 W after a failed release. +# r. (round 3) Start does not clear a failed streak. +# s. (round 3) _hand_back writes 0 W itself instead of calling the hook.