MAINT-13 round 2: gate open when nothing holds the charge back; safety93 scenario restored
checks / unit (pull_request) Failing after 34m45s
infra-pin / fresh (pull_request) Successful in 2s

- gate_open is true with no price gate set up, in a cheap window, or while the
  pack is taking charge; the stall rule and timeout now work on a no-tariff site.
- reset the save timer on a phase change; exact 5 min gap-cap test.
- test_safety93: original timeout scenario kept (B finished) plus a case where a
  due B takes the slot after A's failure.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-08 10:29:46 +02:00
co-authored by Claude Sonnet 5.5
parent 28eff568b5
commit 6f05b2e4be
5 changed files with 106 additions and 15 deletions
+44 -3
View File
@@ -231,10 +231,12 @@ check("...or in a complete charge when the pack is near full",
out and out[0].phase == HOLD)
m = in_charge()
res, t, out = run(m, T0, 10, 70.0)
before = m._trk["open_min"]
res = m.tick(t + 2 * timedelta(hours=1), 70.0)
trk = m._trk
check("a gap in the ticks (add-on down) counts for at most 5 minutes",
trk["open_min"] < 20, )
check("a gap in the ticks (add-on down) counts for exactly 5 minutes (the cap)",
abs(m._trk["open_min"] - before - 5.0) < 1e-9)
m.tick(t + 2 * timedelta(hours=1) + 2 * MIN, 70.0)
check("...and a 2 minute step counts 2", abs(m._trk["open_min"] - before - 7.0) < 1e-9)
print("6. restarts and old state files")
with tempfile.TemporaryDirectory() as d:
@@ -437,6 +439,45 @@ check("...and its failure is on B's keys",
c.maints[1].store.get_time("last_failed") is not None
and c.maints[0].store.get_time("last_failed") is None)
# a site with no price entities and no fixed tariff: nothing holds the charge back
NOTARIFF = {k: v for k, v in OPTS1.items()
if k not in ("price_now_entity", "price_avg_entity")}
c, h = controller(NOTARIFF, False)
m = put_in_charge(c, 0, 5, 100.0)
one_cycle(c)
got = m._trk["open_min"] - 100.0
check("N=1, no tariff and no price entities: the gate counts as open, so the "
f"3 minutes count (+{got:.1f}) although `cheap` is false",
not c.cheap and 2.5 < got < 3.5)
c, h = controller(NOTARIFF, False)
m = put_in_charge(c, 0, 40, CHARGE_TIMEOUT_MIN + 1)
one_cycle(c)
check("...and a pack past its charging time there fails at once (not 7 days)",
m.phase == IDLE and len(notes(h)) == 1)
c, h = controller(OPTS1, False)
c.inverters[0].batt = -800.0
check("price gate closed but the pack is taking 800 W (sun): gate open",
c._charge_gate_open(c.inverters[0]) is True)
c.inverters[0].batt = -10.0
check("...closed and about idle: gate closed",
c._charge_gate_open(c.inverters[0]) is False)
c.inverters[0].batt = None
check("...no battery reading: gate closed", c._charge_gate_open(c.inverters[0]) is False)
c, h = controller(OPTS1, True)
c.cheap = True
check("price gate open: gate open", c._charge_gate_open(c.inverters[0]) is True)
c, h = controller(dict(NOTARIFF, static_profile="x"), False)
check("a fixed tariff set up counts as a gate (closed outside its cheap hours)",
c._charge_gate_open(c.inverters[0]) is False)
# the save timer is reset on a phase change
mm = in_charge()
mm.tick(T0 + MIN, 70.0)
saved_before = mm._trk_saved
mm.abort(T0 + 2 * MIN, "operator")
check("leaving CHARGE resets the save timer along with the record",
saved_before is not None and mm._trk is None and mm._trk_saved is None)
print("9. the words")
en, nl = messages.maint_failed("charge", 7), messages.maint_failed("drain", 7)
for n_ in (en, nl):