SAFETY-90 review: count the start-up seconds like a gap
While the add-on is down the batteries are at 0 W (it sets 0 W when it stops; the ESP32 watchdog after 30 s), so the import before its first reading was the house, not the aim. Those seconds now count at gap_w(limit, the import seen first), and after a restart the restored reading's held 30 s is topped up the same way. Status row: "counted as high use". Tests take the pre-start import as the house (batteries at 0 W) and add restarts of 10/60/150 s with a restored averager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -955,6 +955,12 @@ class Controller:
|
||||
self._gap_held_ws = 0.0
|
||||
self._gap_pre = None
|
||||
self._unseen = None
|
||||
# A held sample TARIFF-01 restored from before this restart: the
|
||||
# averager credits it max_hold_s into the restart, at 0 W batteries.
|
||||
_held = self.p1.averager.held_since
|
||||
self._run_pre = (None if _held is None
|
||||
else (_held, self.p1.averager.held_net_w))
|
||||
self._unseen_held_ws = 0.0
|
||||
self.peak_gap_s = 0.0
|
||||
self.peak_unmeasured_s = 0.0
|
||||
self._peak_alarm_block = None
|
||||
@@ -2706,16 +2712,18 @@ class Controller:
|
||||
def _fill_peak_gaps(self, proj, now, reading):
|
||||
"""SAFETY-90: `proj` with a meter gap's uncounted seconds counted at
|
||||
`peakguard.gap_w()` of the imports seen just before and just after it,
|
||||
and those before this run's first reading at the aim (shown as not
|
||||
measured). DECISIONS/SAFETY-90-peak-guard-after-a-gap.md."""
|
||||
and those before this run's first reading likewise at the import seen
|
||||
first (shown as not measured).
|
||||
DECISIONS/SAFETY-90-peak-guard-after-a-gap.md."""
|
||||
self.peak_gap_s = self.peak_unmeasured_s = 0.0
|
||||
avg = self.p1.averager
|
||||
since = avg.held_since
|
||||
if proj is None or proj.block_start is None or since is None:
|
||||
return proj
|
||||
if self._unseen is None and self.p1.last is not None:
|
||||
self._unseen = (proj.block_start, max(0.0, (
|
||||
since - proj.block_start).total_seconds() - avg.elapsed_s))
|
||||
self._unseen = [proj.block_start, max(0.0, (
|
||||
since - proj.block_start).total_seconds() - avg.elapsed_s),
|
||||
since, None]
|
||||
seen = (None if self.grid is None or self.site_batt_w is None
|
||||
else float(self.grid) + max(0.0, float(self.site_batt_w)))
|
||||
if proj.block_start != self._gap_block:
|
||||
@@ -2723,12 +2731,25 @@ class Controller:
|
||||
self._gap_filled_s = self._gap_held_ws = 0.0
|
||||
cap = float(self.o.get("peak_cap_w", 3500))
|
||||
ceiling = peakguard.quarter_ceiling_w(cap, reading)[0]
|
||||
unseen_on = (self._unseen is not None
|
||||
and self._unseen[0] == proj.block_start)
|
||||
if unseen_on and self._unseen[3] is None and seen is not None:
|
||||
# While the add-on was down the batteries were at 0 W, so the
|
||||
# seconds before its first reading count like a gap, at the
|
||||
# import seen first; a restored reading's held part is topped up.
|
||||
self._unseen[3] = peakguard.gap_w(ceiling, seen)
|
||||
if self._run_pre is not None:
|
||||
s0, g0 = self._run_pre
|
||||
h = (min(s0 + timedelta(seconds=avg.max_hold_s),
|
||||
self._unseen[2])
|
||||
- max(s0, proj.block_start)).total_seconds()
|
||||
self._unseen_held_ws = max(0.0, h) * max(
|
||||
0.0, self._unseen[3] - max(0.0, g0))
|
||||
kw = dict(tail_s=(now - since).total_seconds(),
|
||||
max_hold_s=avg.max_hold_s,
|
||||
unseen_s=(self._unseen[1] if self._unseen is not None
|
||||
and self._unseen[0] == proj.block_start else 0.0),
|
||||
unseen_w=peakguard.quarter_target_w(
|
||||
cap, self.peak_margin_w, reading))
|
||||
unseen_s=self._unseen[1] if unseen_on else 0.0,
|
||||
unseen_w=(self._unseen[3] if unseen_on
|
||||
and self._unseen[3] is not None else ceiling))
|
||||
_p, gap_s, _u = peakguard.fill_gaps(proj, gap_w=0.0, **kw)
|
||||
if gap_s > self._gap_filled_s + peakguard.GAP_TOL_S:
|
||||
self._gap_w = peakguard.gap_w(ceiling, self._gap_w,
|
||||
@@ -2752,7 +2773,8 @@ class Controller:
|
||||
self._gap_pre = (since, float(self.grid))
|
||||
out, self.peak_gap_s, self.peak_unmeasured_s = peakguard.fill_gaps(
|
||||
proj, gap_w=self._gap_w if self._gap_w is not None else ceiling,
|
||||
held_ws=self._gap_held_ws, **kw)
|
||||
held_ws=self._gap_held_ws + (self._unseen_held_ws if unseen_on
|
||||
else 0.0), **kw)
|
||||
return out
|
||||
|
||||
def _peak_over_drain(self, claims, maint_results) -> None:
|
||||
@@ -6458,7 +6480,7 @@ class Controller:
|
||||
peakguard.blind_kind(_pg.blind))})
|
||||
elif self.peak_unmeasured_s > 0.0:
|
||||
# SAFETY-90: the start of this quarter came before the first
|
||||
# reading, and the guard counts it at the aim.
|
||||
# reading, and the guard counts it as high use.
|
||||
out.append({"ok": False, "warn": True, "id": "peak_guard",
|
||||
"text": messages.row_peak_guard_unmeasured()})
|
||||
elif _pg.engaged:
|
||||
|
||||
Reference in New Issue
Block a user