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
+12 -8
View File
@@ -9,16 +9,18 @@ Learned per unit, never configured:
- only while control runs, the unit is not refused, stale or tripped, and
its command has been within IDLE_ASK_W for IDLE_QUIET_S (a refusing battery
is asked more than that, so it cannot teach its refusal as idle);
- a reading beyond IDLE_MAX_W is not a sample, and the value is clamped to it;
- a reading beyond IDLE_SAMPLE_W is not a sample;
- first value: the mean of IDLE_FIRST_S of samples; then a slow follow
(IDLE_TAU_S);
- persisted per unit and restored, clamped, at start.
Until learned the idle reading is 0 W, the rule as it was.
- persisted per unit and restored, clamped to IDLE_SAMPLE_W, at start.
The band is centred on it clamped to IDLE_MAX_W (owner, 2026-10-04). Until
learned the idle reading is 0 W, the rule as it was.
"""
import math
IDLE_MAX_W = 250.0 # |idle| bound: a band centred further out hides too much
IDLE_MAX_W = 75.0 # |idle| used: under the 100 W band, so it keeps 0 W +-25 W
IDLE_SAMPLE_W = 250.0 # a reading beyond this is not a sample
IDLE_ASK_W = 50.0 # a command this small counts as "asked ~0"
IDLE_QUIET_S = 120.0 # 6 lags of 20 s: a 2500 W step has decayed to 6 W
IDLE_FIRST_S = 120.0 # seconds of samples averaged for the first value
@@ -41,7 +43,7 @@ class IdleReading:
def __init__(self, saved=None):
v = _num(saved)
self.value = (None if v is None
else max(-IDLE_MAX_W, min(IDLE_MAX_W, v)))
else max(-IDLE_SAMPLE_W, min(IDLE_SAMPLE_W, v)))
self.saved = self.value
self.quiet_since = None
self.last = None
@@ -50,7 +52,10 @@ class IdleReading:
@property
def w(self) -> float:
return 0.0 if self.value is None else self.value
"""The centre used: the learned value clamped to IDLE_MAX_W."""
if self.value is None:
return 0.0
return max(-IDLE_MAX_W, min(IDLE_MAX_W, self.value))
def observe(self, reading, standing_w, now_mono, may_learn) -> bool:
"""One read. Returns True when the value should be persisted."""
@@ -66,7 +71,7 @@ class IdleReading:
self.quiet_since = now
b = _num(reading)
if (now - self.quiet_since < IDLE_QUIET_S or b is None
or abs(b) > IDLE_MAX_W or dt <= 0.0):
or abs(b) > IDLE_SAMPLE_W or dt <= 0.0):
return False
if self.value is None:
self.acc_w += b * dt
@@ -76,7 +81,6 @@ class IdleReading:
self.value = self.acc_w / self.acc_s
else:
self.value += (b - self.value) * min(1.0, dt / IDLE_TAU_S)
self.value = max(-IDLE_MAX_W, min(IDLE_MAX_W, self.value))
if self.saved is None or abs(self.value - self.saved) >= IDLE_SAVE_W:
self.saved = self.value
return True