MAINT-13: a full pack that stops at 98-99 % is not a failed charge
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:
co-authored by
Claude Opus 5.5
parent
b527e99f13
commit
28eff568b5
@@ -77,7 +77,7 @@ from .inverter import (LINK_STALE_S, UNIT_A, build_roster,
|
||||
roster_entries, roster_names)
|
||||
from .maintenance import (
|
||||
CHARGE, DRAIN, HOLD, IDLE, POWER_MOVING, MaintConfig, Maintenance,
|
||||
MaintResult, arbitrate as maint_arbitrate, site_zone,
|
||||
FAIL_BACKOFF_DAYS, MaintResult, arbitrate as maint_arbitrate, site_zone,
|
||||
)
|
||||
from .mqtt import (MODE_CONFIGS_KEY, MODE_LABELS, USE_OFF, USE_ON,
|
||||
MqttPublisher, announced_units, mode_configs,
|
||||
@@ -3923,10 +3923,24 @@ class Controller:
|
||||
if blocked is None:
|
||||
maint_results.append(MaintResult(IDLE, False, None, False, []))
|
||||
continue
|
||||
res = machine.tick(now, inv.soc, blocked_by=blocked)
|
||||
# MAINT-13: the charge timeout counts only the minutes the price
|
||||
# gate (`self.cheap`) let the pack charge.
|
||||
res = machine.tick(now, inv.soc, blocked_by=blocked,
|
||||
gate_open=bool(self.cheap))
|
||||
for msg in res.events:
|
||||
self.log_event(msg, hint=messages.hint("maintenance",
|
||||
phase=machine.phase))
|
||||
self.log_event(msg, hint=messages.hint(
|
||||
"maint_failed" if res.failed else "maintenance",
|
||||
phase=machine.phase))
|
||||
if res.failed:
|
||||
# Said once: only the tick that failed carries `res.failed`.
|
||||
multi = len(self.inverters) > 1
|
||||
slug = (unit_slugs(self.units)[self.inverters.index(inv)]
|
||||
if multi else "battery")
|
||||
await self.hass.call(
|
||||
"persistent_notification", "create",
|
||||
messages.maint_failed(
|
||||
res.failed, FAIL_BACKOFF_DAYS,
|
||||
unit=inv.name if multi else "", slug=slug).data())
|
||||
maint_results.append(res)
|
||||
# ⚠️ CARRIED TO THE STATUS PAGE, NOT LEFT IN THE 40-LINE ACTION LOG. The
|
||||
# deferral is edge-logged (it lasts as long as the other pack's cycle),
|
||||
@@ -7100,6 +7114,7 @@ class Controller:
|
||||
# date, unchanged.
|
||||
_now = datetime.now(timezone.utc)
|
||||
_dues = [m.next_due(_now) for m in self.maints]
|
||||
_waits = [u for u in (m.backoff_until(_now) for m in self.maints) if u]
|
||||
next_due = None if any(d is None for d in _dues) else min(_dues)
|
||||
rows = [
|
||||
("Grid", w(self.grid)),
|
||||
@@ -7147,6 +7162,10 @@ class Controller:
|
||||
*((("Maintenance waiting", "; ".join(f"{n}: {why}"
|
||||
for n, why in self._maint_deferred)),)
|
||||
if self._maint_deferred else ()),
|
||||
# MAINT-13: only while a failed cycle waits, so a site with nothing
|
||||
# failed carries exactly the rows it carried before.
|
||||
*((("Maintenance paused", messages.row_maint_waits(
|
||||
min(_waits).strftime("%Y-%m-%d"))),) if _waits else ()),
|
||||
("Maintenance due", "now" if next_due is None else next_due.strftime("%Y-%m-%d")),
|
||||
("Cheap window", "yes" if self.cheap else "no"),
|
||||
("Tariff", self.tariff_words),
|
||||
|
||||
Reference in New Issue
Block a user