MAINT-11: start or stop the maintenance cycle per battery
checks / unit (pull_request) Successful in 38m46s
infra-pin / fresh (pull_request) Successful in 1s

maint_start and maint_abort take an optional unit. With two or more batteries
each Batteries card has Start cycle and Stop cycle buttons. One slot per site
stays: a start while a battery cycles is refused and names it (no queue). A
battery that is switched off, an unknown unit, and a stop on a battery that is
not cycling are refused with a plain message. A request with no unit is the
old call. The scheduler and the phase machine are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-13 21:37:36 +02:00
co-authored by Claude
parent 79dd0770f9
commit 21d8a944bc
9 changed files with 521 additions and 1 deletions
+41
View File
@@ -2777,6 +2777,43 @@ class Controller:
out["sell"] = pm.sell
return out
async def _maint_unit_action(self, what, unit, now) -> dict:
"""MAINT-11: start or abort the maintenance cycle of ONE named battery.
Returns `{"error", "code", "unit"}` for a refusal and changes nothing.
The site keeps its one maintenance slot: a start while any battery
holds it is refused (no queue), see DECISIONS/MAINT-11. The phase
machine is not touched beyond `force_start()` and `abort()`, the same
two calls the bare button makes.
"""
inv = next((i for i in self.inverters if i.name == unit), None)
if inv is None:
return self._maint_refusal("unit")
machine = self.maints[self.inverters.index(inv)]
if what == "maint_start":
holder = next((i for i, m in zip(self.inverters, self.maints)
if m.phase in POWER_MOVING), None)
if holder is not None:
return self._maint_refusal("busy", holder.name)
if self.modes_on and not self.pack_mode(inv).use:
return self._maint_refusal("off", inv.name)
for msg in machine.force_start(now):
self.log_event(msg, hint=messages.hint(
"maintenance", phase=machine.phase))
return {"unit": inv.name, "phase": machine.phase}
if machine.phase not in POWER_MOVING:
return self._maint_refusal("idle", inv.name)
for msg in machine.abort(now, "operator"):
self.log_event(msg, hint=messages.hint("maintenance",
phase=machine.phase))
await self.command_site_zero()
return {"unit": inv.name, "phase": machine.phase}
@staticmethod
def _maint_refusal(code, unit="") -> dict:
return {"error": messages.maint_refused(code, unit), "code": code,
"unit": unit}
def mode_values(self) -> dict:
"""OPT-11 P2: each battery's mode and switch, for their HA state
topics. Read from the store, so they are right after a restart. Empty
@@ -7513,6 +7550,10 @@ class Controller:
# would read as the stop button not working.
await self.command_site_zero()
return {"auto": self.auto}
if what in ("maint_start", "maint_abort") and unit is not None:
# MAINT-11: the operator named the battery. A request with no unit
# falls through to the branches below, unchanged.
return await self._maint_unit_action(what, unit, now)
if what == "maint_start":
# ⚠️ WHICH PACK, BECAUSE THERE ARE NOW SEVERAL AND THE BUTTON HAS
# ONE LABEL. `_maint_button()` answers it: the pack that holds the