SAFETY-118: a missing peak forecast reading keeps the unit-fault state
checks / unit (pull_request) Successful in 40m19s
infra-pin / fresh (pull_request) Successful in 1s

A missing reading (unavailable, unknown, failed read, non-numeric) no longer
clears the fault: no false 'used again' INFO, no repeated WARNING, the status
row stays. Adds pv.has_reading(). DECISIONS wording fixed; tests added.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-16 18:19:44 +02:00
co-authored by Claude
parent f0b4e68e49
commit 7d52dc1c0f
4 changed files with 97 additions and 16 deletions
+78 -6
View File
@@ -178,15 +178,22 @@ check("no unit is a different cause and warns", len(warns()) == 3
and "has no unit" in warns()[2])
reads(c)
check("...and only once", len(warns()) == 3)
USED = (logging.INFO, "The 15-minute demand forecast is used again.")
def used():
return [r for r in rec.rows if r == USED]
h.fc = st("unavailable")
reads(c)
check("unavailable is no unit fault and adds no warning", len(warns()) == 3)
check("unavailable adds no warning and no 'used again'",
len(warns()) == 3 and not used())
h.fc = st(820, "W")
reads(c)
check("a good unit is read again and logs INFO, not a warning",
c.peak_fc == 820.0 and len(warns()) == 3
and rec.rows[-1] == (logging.INFO,
"The 15-minute demand forecast is used again."))
check("a good unit is read again: one INFO, no new warning",
c.peak_fc == 820.0 and len(warns()) == 3 and len(used()) == 1
and rec.rows[-1] == USED)
n = len(warns())
for s in (st(820, "W"), st(0.8, "kW")):
h.fc = s
@@ -291,7 +298,72 @@ print(" NL bad unit:", messages.peak_forecast_unit("kWh", "nl"))
check("the strings differ by cause and by language",
len({messages.peak_forecast_unit(u, l) for u in ("", "kWh")
for l in ("en", "nl")}) == 4)
check("pv.unit_fault: None for a good unit and for a missing reading",
print("6. a missing reading leaves the unit fault as it was (F1)")
lg.addHandler(rec)
def prow(c):
return [r for r in c.checks() if r.get("id") == "peak_forecast"]
def flap(first, gap, last):
"""first, gap, first again (or last): the log rows and the status row
after each cycle."""
rec.rows.clear()
c, h = make(first)
out = []
for s in (first, gap, last):
h.fc = s
reads(c)
out.append((len(warns()), len(used()), len(prow(c)), c.peak_fc))
return out
for label, first, gap in [
("kWh -> unavailable -> kWh", st(820, "kWh"), st("unavailable", "kWh")),
("kWh -> unknown -> kWh", st(820, "kWh"), st("unknown", "kWh")),
("kWh -> failed read -> kWh", st(820, "kWh"), None),
("kWh -> non-numeric -> kWh", st(820, "kWh"), st("sunny", "kWh")),
("no unit -> failed read -> no unit", st(820), None),
("no unit -> unavailable -> no unit", st(820), st("unavailable"))]:
r = flap(first, gap, first)
check(f"{label}: one WARNING, no 'used again', row on all 3 ({r})",
[x[0] for x in r] == [1, 1, 1] and [x[1] for x in r] == [0, 0, 0]
and [x[2] for x in r] == [1, 1, 1]
and [x[3] for x in r] == [None, None, None])
r = flap(st(820, "kWh"), st("unavailable", "kWh"), st(820, "W"))
check(f"kWh -> unavailable -> W: the fault clears only on the W reading ({r})",
[x[1] for x in r] == [0, 0, 1] and [x[2] for x in r] == [1, 1, 0]
and [x[0] for x in r] == [1, 1, 1] and r[2][3] == 820.0)
rec.rows.clear()
c, h = make(st(820, "W"))
for s in (st(820, "W"), st("unavailable", "W"), None, st("sunny", "W")):
h.fc = s
reads(c)
check(f"good unit, reading {s and s['state']!r}: no row, no log",
not prow(c) and not warns() and not used())
rec.rows.clear()
c, h = make(st(820, "kWh"))
reads(c)
for s in (st("unavailable", "kWh"), None):
h.fc = s
reads(c)
check("fault row stays while the reading is missing",
len(prow(c)) == 1 and prow(c)[0]["text"]
== messages.row_peak_forecast_unit("kWh"))
c, h = make(st(820, "W"))
h.fc = st("unavailable", "W")
reads(c)
check("good-unit sensor only unavailable: the fault row stays away",
not prow(c))
lg.removeHandler(rec)
check("has_reading: numbers only",
pv.has_reading(st(0, "kWh")) and pv.has_reading(st("1.5"))
and not pv.has_reading(None) and not pv.has_reading(st("unavailable"))
and not pv.has_reading(st("unknown")) and not pv.has_reading(st("x")))
check("pv.unit_fault: None for a good unit and for a missing reading (has_reading tells them apart)",
pv.unit_fault(st(1, "kW")) is None and pv.unit_fault(None) is None
and pv.unit_fault(st("unavailable")) is None
and pv.unit_fault(st(1)) == "" and pv.unit_fault(st(1, "kWh")) == "kWh")