SAFETY-97: centre the band on the idle reading clamped to 75 W
checks / unit (pull_request) Successful in 29m26s
infra-pin / fresh (pull_request) Failing after 2s

Owner decision 2026-10-04 (review R1). A wrong idle reading more than
about 100 W off hid every refusal on that battery. Clamped under the band,
the band keeps 0 W +-25 W, so a battery reading about 0 W while refusing is
caught whatever was learned or restored. Samples are still taken to 250 W.
Beyond about +-170 W of idle a battery behaves as in 0.14.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 16:57:16 +02:00
co-authored by Claude Opus 5.5
parent 88ebf3a5a6
commit 7d06aa674d
3 changed files with 103 additions and 31 deletions
+62 -9
View File
@@ -169,7 +169,7 @@ def small_ask():
b, b_old = small_ask(), without_fix(small_ask)
check("SAFETY-42: B giving nothing at a small ask, idle -100 W, is parked "
"(not latched as not delivering in 700 s; with idle 0 W it is)",
not b.delivery_wd.tripped and b.idle_w == -100.0
not b.delivery_wd.tripped and b.idle.value == -100.0
and b_old.delivery_wd.tripped)
# ============================================================================ #
@@ -213,8 +213,11 @@ check("⭐ a charge reversing into a discharge (lag 10 and 20 s, 1 s meter) "
print("\n3. learning the idle reading")
_t, r = caught(2, "b", 1.0)
c = r["c"]
check("learned during the rest: A about 0 W, B about -100 W, and saved",
abs(c.inverters[0].idle_w) < 5 and abs(c.inverters[1].idle_w + 100) < 5
check("learned during the rest: A about 0 W, B about -100 W (used: -75 W), "
"and saved",
abs(c.inverters[0].idle_w) < 5
and abs(c.inverters[1].idle.value + 100) < 5
and c.inverters[1].idle_w == -idle.IDLE_MAX_W
and abs(c.store.data.get(unit_key("inverter-b", "idle_w"), 0) + 100)
< 5)
@@ -250,10 +253,11 @@ try:
finally:
rig.Plant = real_plant
b = r["c"].inverters[1]
print(f" refusing B reading -160 W: refused at {_t} s, idle {b.idle_w:.1f}")
print(f" refusing B reading -160 W: refused at {_t} s, idle "
f"{b.idle.value:.1f}")
check("⭐ not learned while refused: 800 s held at 0 W reading -160 W leaves "
"B's idle reading at -100 W",
_t is not None and b.refused and abs(b.idle_w + 100.0) < 1.0)
_t is not None and b.refused and abs(b.idle.value + 100.0) < 1.0)
# Not learned while control is stopped (another owner may drive the battery)
# or while the unit's sign watchdog has latched.
@@ -319,12 +323,58 @@ for t in range(0, 600):
check("a reading beyond 250 W is not a sample (-300 W: never learned; a "
"register held at 2000 W then -240 W: -240 W)",
o.value is None and p.value is not None and abs(p.value + 240.0) < 1e-6)
check("a saved value is clamped to 250 W, and garbage is ignored",
idle.IdleReading(900).w == idle.IDLE_MAX_W
check("a saved value is clamped to 250 W, the band's centre to 75 W, and "
"garbage is ignored",
idle.IdleReading(900).value == idle.IDLE_SAMPLE_W
and idle.IdleReading(900).w == idle.IDLE_MAX_W
and idle.IdleReading(-900.0).w == -idle.IDLE_MAX_W
and idle.IdleReading(-60.0).w == -60.0
and idle.IdleReading("x").value is None
and idle.IdleReading(float("nan")).value is None)
# ============================================================================ #
print("\n3b. a wrong idle reading (owner, 2026-10-04: centre clamped to 75 W)")
def wrong_idle(n, unit, err, restore):
"""Seconds from the cut to the refusal when the idle reading is `err` W
off: taught wrong during the rest, or restored wrong."""
cu = "a" if n == 1 else unit
true = -100.0 if unit == "b" else 0.0
if restore:
return caught(n, unit, 1.0, store={
unit_key(f"inverter-{cu}", "idle_w"): true + err})[0]
class _W(rig.Plant):
def advance(self, t):
rig.OFFSET[cu] = true + (err if t < REST else 0.0)
super().advance(t)
saved, real = dict(rig.OFFSET), rig.Plant
rig.Plant = _W
try:
return caught(n, unit, 1.0)[0]
finally:
rig.Plant = real
rig.OFFSET.clear()
rig.OFFSET.update(saved)
A_WRONG = {(n, e, rs): wrong_idle(n, "a", e, rs) for n in (1, 2)
for e, rs in ((-250.0, False), (-150.0, False), (150.0, False),
(250.0, False), (-250.0, True), (200.0, True),
(250.0, True))}
print(f" A (reads about 0 W) refusing, idle off by E: {A_WRONG}")
check("⭐ a battery reading about 0 W while refusing is caught whatever idle "
"reading was learned or restored (A, E -250..+250 W, N=1 and N=2)",
all(v is not None and v <= 32.0 for v in A_WRONG.values()))
check("⭐ ...including a wrong saved idle_w of +200 W on A",
all(A_WRONG[(n, 200.0, True)] is not None for n in (1, 2)))
B_WRONG = {(n, e): wrong_idle(n, "b", e, False) for n in (1, 2)
for e in (-150.0, -50.0, 90.0)}
print(f" B refusing, idle taught wrong by E: {B_WRONG}")
check("B is caught across -150..+90 W of error in its idle reading",
all(v is not None and v <= 32.0 for v in B_WRONG.values()))
# ============================================================================ #
print("\n4. the rules at their edges")
@@ -378,8 +428,11 @@ check("compute(): a charge refusal ends when the battery takes 100 W more "
and control.compute(actual_w=-205.0, idle_w=-100.0, **args).refused
== 0
and control.compute(actual_w=-110.0, **args).refused == 0)
check("the idle bound is under the band's reach of a real delivery: 250 W",
idle.IDLE_MAX_W == 250.0 and idle.IDLE_ASK_W < control.PROBE_W)
check("the band's centre stays within 75 W of 0 W, so the band keeps 0 W "
"+-25 W; samples to 250 W",
idle.IDLE_MAX_W == 75.0 and idle.IDLE_SAMPLE_W == 250.0
and control.REFUSED_BELOW_W - idle.IDLE_MAX_W >= 25.0
and idle.IDLE_ASK_W < control.PROBE_W)
print()
if fails: