CI-02 probe F: the ORIGINAL gate, unchanged, on the same unmergeable PR
checks / unit (pull_request) Successful in 3m38s

This commit is contained in:
glenn schrooyen
2026-09-01 03:38:00 +02:00
parent 4c64aa2106
commit 6ff223d44b
25 changed files with 1269 additions and 217 deletions
+62 -7
View File
@@ -42,7 +42,7 @@ from .clamp import (
is_commandable,
)
from .freeze import (
FreezeWatchdog, FreezeWatchdogConfig, looks_frozen, rail_w,
FreezeWatchdog, FreezeWatchdogConfig, looks_frozen, rail_unreachable, rail_w,
)
from .hass import HomeAssistant
from .maintenance import IDLE, MaintConfig, Maintenance
@@ -557,8 +557,14 @@ class Controller:
"peak_soc_floor",
peakguard.PEAK_OPTION_DEFAULTS["peak_soc_floor"])),
max_hold_s=self.p1.averager.max_hold_s,
# ⚠️ SAFETY-20. The device range goes in TOO, and this call site is
# the one that needed it most: `standing` is `last_write`, i.e. the
# value AFTER `clamp_to_rating()` applied `[dev_min, dev_max]`, so
# grading it against a rail derived from bare `max_w` compares a
# device-clipped number to a ceiling the device cannot reach.
frozen=looks_frozen(getattr(self.p1_source, "unchanged_s", None),
standing, max_w, self.freeze_wd.cfg),
standing, max_w, self.freeze_wd.cfg,
dev_min=self.dev_min, dev_max=self.dev_max),
)
peak_claim = (peakguard.claim(self.peak_guard)
if self.peak_guard is not None else None)
@@ -1035,7 +1041,9 @@ class Controller:
unchanged = getattr(self.p1_source, "unchanged_s", None)
fired = self.freeze_wd.evaluate(unchanged, self.target,
float(self.o.get("max_w", 2000)),
datetime.now(timezone.utc))
datetime.now(timezone.utc),
dev_min=self.dev_min,
dev_max=self.dev_max)
if not fired:
return
self.log_event("METER FROZEN? " + self.freeze_wd.reason)
@@ -1270,7 +1278,20 @@ class Controller:
self.mqtt.close()
# -- UI ------------------------------------------------------------------
def checks(self) -> list:
def checks(self, when: datetime | None = None) -> list:
"""The status rows. `when` is the reference time for anything billed by
the calendar - today's month when it is None, which is what the live
page means; `status_payload()` calls this with no argument.
⚠️ IT EXISTS SO THE PEAK ROW CAN BE GRADED (TEST-17). Every other peak
query already takes it - `PeakStore.peak()`, `PeakTracker.peak()`,
`headroom_w()`, `warning_frac()` - and the row built from them was the
one thing that could only be read against `datetime.now()`. A suite
that banks an August quarter then asks this page what the peak is goes
green in August and red in September, which is what happened at 00:00
on 2026-09-01: TARIFF-01's own section 10, red on a calendar boundary,
on a commit nobody had touched for a day.
"""
o = self.o
out = []
if self.p1_enabled:
@@ -1319,26 +1340,60 @@ class Controller:
f"meter_source {o.get('meter_source')} - only "
f"homewizard_local sees whether the meter's own "
f"document changes"})
elif rail_unreachable(o.get("max_w", 2000),
dev_min=self.dev_min, dev_max=self.dev_max):
# SAFETY-20, and THE SECOND REASON TO USE THIS ROW RATHER THAN A
# NEW MECHANISM. The transport case above and this one are the
# same statement - "the conjunction cannot be evaluated here" -
# and an operator does not need to learn two vocabularies for
# it. What differs is only which term is missing: there,
# `unchanged_s`; here, a rail the command could ever stand at.
out.append({"ok": False, "warn": True,
"text": f"freeze detector: NOT COVERED - "
+ rail_unreachable(
o.get("max_w", 2000),
dev_min=self.dev_min,
dev_max=self.dev_max)})
else:
# ⚠️ SAFETY-20. THE RAIL PRINTED HERE IS THE RAIL THE PREDICATE
# USES, device range included. This row used to interpolate
# `rail_w(max_w)` while `looks_frozen()` graded a command the
# device had already clipped below it, so on any install whose
# inverter is smaller than its configured ceiling the page named
# a number the command could never reach and called it `armed`.
# Both the row and the predicate now read the same three inputs.
_rail = rail_w(o.get("max_w", 2000),
dev_min=self.dev_min, dev_max=self.dev_max)
# And when the device is what set that number, SAY SO - the
# operator configured `max_w` and is owed the reason the guard
# is armed somewhere else.
# ⚠️ Either bound may be None on its own - `hass.limits()`
# reads `min` and `max` independently and returns None for
# whichever attribute is missing or unparseable - so this
# renders whatever is there rather than formatting a None.
_cap = ("" if _rail >= rail_w(o.get("max_w", 2000))
else f" - below max_w because "
f"{o.get('setpoint_entity') or 'the setpoint device'}"
f" accepts only {self.dev_min}...{self.dev_max} W")
out.append({"ok": True, "warn": False,
"text": f"freeze detector: armed (alerts after "
f"{self.freeze_wd.cfg.hold_s:.0f} s unchanged "
f"while the command is over "
f"{rail_w(self.o.get('max_w', 2000)):.0f} W)"})
f"{_rail:.0f} W{_cap})"})
# TARIFF-01. The month's billed peak, and - the load-bearing half -
# a DIFFERENT row when the store could not answer. "no peak yet" is
# a normal state on the 1st; "unreadable" is a broken instrument, and
# a status page that spelled them the same way would let a 0 W read
# look like a quiet month right up until SAFETY-07 shaved nothing.
_pk = self.peak.peak()
_pk = self.peak.peak(when)
if _pk.is_unreadable:
out.append({"ok": False, "warn": True,
"text": "capacity-tariff peak: " + _pk.explain()})
else:
# §9.1's "warning entity at 80 %", as a status row rather than
# a second entity - see the constructor on not doubling up.
_frac = self.peak.warning_frac() or 0.0
_frac = self.peak.warning_frac(when) or 0.0
_ok = _frac < 0.8
out.append({"ok": _ok, "warn": not _ok,
"text": _pk.explain()