SMA-10 round 3: Start clears a failed hand-back streak; the hook is tested
checks / unit (pull_request) Successful in 23m23s
infra-pin / fresh (pull_request) Successful in 2s

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 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-07 21:31:28 +01:00
co-authored by Claude
parent d26cc573fe
commit 313503724d
2 changed files with 61 additions and 1 deletions
+7 -1
View File
@@ -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 -----
@@ -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.