CI-02 probe F: the ORIGINAL gate, unchanged, on the same unmergeable PR
checks / unit (pull_request) Successful in 3m38s
checks / unit (pull_request) Successful in 3m38s
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user