MAINT-13: a full pack that stops at 98-99 % is not a failed charge
checks / unit (pull_request) Successful in 34m21s
infra-pin / fresh (pull_request) Successful in 1s

A charge is complete at SoC >= target, or at >= target - 3 once the SoC has
made no new high for 60 min of allowed charging, or when the charging time
runs out at >= target - 3. The 36 h timeout counts only the minutes the price
gate let the pack charge (plus a 7 day wall-clock backstop). A cycle that
fails (stuck drain, charge below target - 3) backs off 7 days, tells the
operator once (EN + NL), and no cycle starts on that pack in that time unless
the operator forces one. State survives a restart; old state files load.

No option added. DECISIONS/MAINT-13-full-pack-is-complete.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-08 09:36:06 +02:00
co-authored by Claude Opus 5.5
parent b527e99f13
commit 28eff568b5
9 changed files with 776 additions and 16 deletions
+29 -1
View File
@@ -102,6 +102,7 @@ class Notice:
title: str
body: str
section: str = "" # the DOCS.md heading, when it is not the title itself
body_nl: str = "" # MAINT-13: the same words in Dutch, shown under the English
@property
def docs(self) -> str:
@@ -110,7 +111,9 @@ class Notice:
def data(self) -> dict:
"""The `persistent_notification.create` payload."""
return {"notification_id": self.id, "title": self.title,
"message": f"{self.body}\n\n{details(self.docs)}"}
"message": f"{self.body}\n\n"
+ (f"{self.body_nl}\n\n" if self.body_nl else "")
+ details(self.docs)}
# --- the catalogue ----------------------------------------------------------
@@ -124,6 +127,8 @@ T_CIRCULATING = "Inverters working against each other"
T_NOT_DELIVERING = "{unit} is not delivering"
S_NOT_DELIVERING = "An inverter is not delivering"
T_METER_FROZEN = "Meter reading may be stuck"
T_MAINT_FAILED = "Battery cycle did not finish"
T_MAINT_FAILED_NL = "Batterijcyclus niet afgerond"
ID_LOOP_BOUND = "goodwe_integrator_bound"
ID_OUTPUT_CLAMP = "goodwe_output_clamp"
@@ -132,6 +137,7 @@ ID_PEAK_RESERVE = "goodwe_peak_soc_floor"
ID_CIRCULATING = "goodwe_circulating_power"
ID_NOT_DELIVERING = "goodwe_not_delivering_{slug}"
ID_METER_FROZEN = "goodwe_meter_frozen"
ID_MAINT_FAILED = "goodwe_maint_failed_{slug}"
# Which limit cut a command (clamp.ClampResult.bound, plus the loop's own).
# The setting is named by its label, "Inverter rating (W)".
@@ -295,6 +301,24 @@ def meter_frozen(unchanged_s) -> Notice:
f"stop control on the add-on page.")
def maint_failed(kind: str, days: int, unit: str = "", slug: str = "") -> Notice:
"""MAINT-13: a monthly cycle stopped before it was done. Said once."""
full = kind == "charge"
who = unit or "The battery"
who_nl = unit or "De batterij"
en = f"{who} did not {'fill up' if full else 'empty'}, so its cycle " \
f"stopped and tries again in {days} days."
nl = f"{who_nl} raakte niet {'vol' if full else 'leeg'}, dus de cyclus is " \
f"gestopt en probeert het over {days} dagen opnieuw."
return Notice(ID_MAINT_FAILED.format(slug=slug or "battery"), WARNING,
T_MAINT_FAILED, en, body_nl=f"{T_MAINT_FAILED_NL}. {nl}")
def row_maint_waits(until_text: str) -> str:
"""MAINT-13: the status row while a failed cycle waits."""
return f"Waits until {until_text} after a cycle that did not finish."
# --- the status page (UX-08 phase 2b) ----------------------------------------
# The rows of the Commissioning list and the banner at the top of the add-on
# page. `main.py` decides WHEN a row shows and its colour (ok / warn / bad);
@@ -947,6 +971,7 @@ HINTS = {
"not_delivering_cleared": "{unit}: not-delivering alarm cleared",
"maintenance": "Battery maintenance: {phase}",
"maint_paused": "Maintenance charging paused to stay under the peak limit",
"maint_failed": "The battery cycle did not finish, so it waits before trying again",
"pack_on": "{unit}: battery switched on",
"pack_off": "{unit}: battery switched off, it is asked for 0 W",
"pack_mode": "{unit}: battery mode changed",
@@ -1118,6 +1143,7 @@ def status_samples() -> list:
("row_battery_refuses", AMBER, row_battery_refuses("inverter-b", 1,
True)),
("row_circulating_check", GREEN, row_circulating_check(0, 0, 60)),
("row_maint_waits", AMBER, row_maint_waits("2026-10-13")),
]
@@ -1148,6 +1174,8 @@ def samples() -> list:
not_delivering("inverter-b", "inverter_b"),
meter_frozen(600),
meter_frozen(90),
maint_failed("charge", 7),
maint_failed("drain", 7, "inverter-b", "inverter_b"),
]