SMA-10 round 2: a failed release sends 0 W once, retries at heartbeat pace
When release() fails (False, raises or hangs past RELEASE_BUDGET_S) the unit gets one bounded write(0) through the new Driver.release_fallback() hook (default write(0)), then nothing more, so the inverter's own fallback timer is not reset. The release is retried once per heartbeat, not on every idle write. One error line and one action-log event per failure streak, one line on recovery. Dutch default "terug naar de eigen sturing". DECISIONS says what a unit is left at. test_messages.py: the RELEASE_BUDGET_S data entry is gone again (the line that needed it is gone). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
a1be9c48b3
commit
d26cc573fe
@@ -147,6 +147,12 @@ WRITE_BUDGET_S = 4.0
|
||||
# to it. A release still running at the bound is abandoned, not awaited.
|
||||
RELEASE_BUDGET_S = WRITE_BUDGET_S
|
||||
|
||||
# SMA-10 round 2. The one fallback write after a failed release (default 0 W,
|
||||
# `Driver.release_fallback()`). Worst case for a unit whose release hangs:
|
||||
# RELEASE_BUDGET_S + FALLBACK_BUDGET_S = 6 s, against the Supervisor's 10 s.
|
||||
FALLBACK_BUDGET_S = 2.0
|
||||
HAND_BACK_WORST_S = RELEASE_BUDGET_S + FALLBACK_BUDGET_S
|
||||
|
||||
# SAFETY-34: a Core-only restart wipes persistent_notification and does NOT
|
||||
# restart this add-on (measured on TEST-23), so a latched alarm is re-asserted
|
||||
# this often. Why: goodwe docs/DECISIONS.md, SAFETY-34.
|
||||
@@ -4065,7 +4071,7 @@ class Controller:
|
||||
await asyncio.wait(pending, timeout=RELEASE_BUDGET_S / 2)
|
||||
return await self._driver(inv).release()
|
||||
|
||||
async def _hand_back(self, inv) -> bool:
|
||||
async def _hand_back(self, inv, *, force: bool = False) -> bool:
|
||||
"""SMA-10. Give ONE unit back: exactly one `release()`, then nothing.
|
||||
|
||||
True when the unit is handed back (now, or already). Never raises: a
|
||||
@@ -4086,27 +4092,54 @@ class Controller:
|
||||
return True
|
||||
if not inv.driver.configured():
|
||||
return False
|
||||
# After a failure the next try waits one heartbeat, so a hung release
|
||||
# costs a cycle its 4 s once per heartbeat, not on every cycle. The
|
||||
# Stop button and shutdown (`force`) always try.
|
||||
now_m = time.monotonic()
|
||||
if not force and now_m < inv.release_retry_at:
|
||||
return False
|
||||
inv.released = True
|
||||
why = ""
|
||||
try:
|
||||
ok = bool(await _bounded(self._release_last(inv),
|
||||
RELEASE_BUDGET_S))
|
||||
except (asyncio.TimeoutError, TimeoutError):
|
||||
ok = False
|
||||
_LOG.error("Handing back %s took over %.0f s and was dropped. It "
|
||||
"is tried again.", inv.name, RELEASE_BUDGET_S)
|
||||
ok, why = False, f"took over {RELEASE_BUDGET_S:.0f} s"
|
||||
except Exception as err: # noqa: BLE001 - one unit never stops the rest
|
||||
ok = False
|
||||
_LOG.error("Handing back %s failed: %s. It is tried again.",
|
||||
inv.name, err)
|
||||
ok, why = False, f"{type(err).__name__}: {err}"
|
||||
if ok:
|
||||
inv.last_write = None
|
||||
if inv.release_fails:
|
||||
_LOG.info("Handing back %s worked again after %d failed "
|
||||
"tries.", inv.name, inv.release_fails)
|
||||
inv.release_fails = 0
|
||||
inv.release_retry_at = 0.0
|
||||
inv.release_zero_sent = False
|
||||
self.log_event(f"{inv.name}: handed back",
|
||||
hint=messages.hint("handed_back", unit=inv.name))
|
||||
else:
|
||||
inv.released = False
|
||||
return True
|
||||
inv.released = False
|
||||
inv.release_fails += 1
|
||||
inv.release_retry_at = now_m + float(self.o.get("heartbeat_s", 10))
|
||||
_LOG.debug("Handing back %s failed (try %d): %s", inv.name,
|
||||
inv.release_fails, why)
|
||||
if inv.release_fails == 1: # one line per failure streak
|
||||
_LOG.error("Handing back %s failed, so it is sent 0 W once and "
|
||||
"handing back is tried again.", inv.name)
|
||||
self.log_event(f"{inv.name}: hand-back failed",
|
||||
hint=messages.hint("hand_back_failed", unit=inv.name))
|
||||
return ok
|
||||
if not inv.release_zero_sent:
|
||||
# ⚠️ ONCE. The unit must not keep carrying its last command, but a
|
||||
# 0 W re-sent on every try would reset the inverter's own fallback
|
||||
# timer for ever. The driver says what the one write is.
|
||||
inv.release_zero_sent = True
|
||||
try:
|
||||
if await _bounded(self._driver(inv).release_fallback(),
|
||||
FALLBACK_BUDGET_S):
|
||||
inv.last_write = 0.0
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOG.debug("The fallback write to %s failed: %r", inv.name, err)
|
||||
return False
|
||||
|
||||
async def _write_unit(self, inv, value: float, *, force: bool = False) -> None:
|
||||
if not inv.driver.configured():
|
||||
@@ -7176,7 +7209,7 @@ class Controller:
|
||||
for inv in self.inverters:
|
||||
inv.target = 0.0
|
||||
await asyncio.gather(*(
|
||||
self._hand_back(inv) if inv.driver.hands_back
|
||||
self._hand_back(inv, force=True) if inv.driver.hands_back
|
||||
else self._write_unit(inv, 0.0, force=True)
|
||||
for inv in self.inverters))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user