Files
goodwe-addon/goodwe_controller/test_opt38.py
T
glenn schrooyenandClaude 0bacafadf0
checks / unit (pull_request) Successful in 23m11s
infra-pin / fresh (pull_request) Successful in 1s
OPT-38 round 2: a kept tomorrow reading is flagged kept only from 20:00
Before the bell ends it replaced today's Home line and the event log with
the kept-for-tonight wording. stand_in() docstring covers the tomorrow case.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-07 06:02:00 +01:00

160 lines
6.7 KiB
Python

"""Runnable check for OPT-38. `python3 test_opt38.py`
Live 0.20.1, 2026-11-06: one of three Forecast.Solar tomorrow sensors last
reported 14:54. From 17:54 (over FRESH_S) to 20:00 (BELL_TO_H) nothing stood in
for it, the OPT-17 sum was dropped and the Forecast tab had no tomorrow. A kept
tomorrow reading now stands in at any hour (`Hold.stand_in()`). Today members,
and the OPT-34 morning rules, are as before.
Mutations (MUTATION.md, purged copies): see the pull request.
"""
import os
import sys
from datetime import datetime, timedelta, timezone
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from app import homeview, solarfc # noqa: E402
fails = []
def check(name, cond):
print(f" {'ok ' if cond else 'FAIL'} {name}")
if not cond:
fails.append(name)
L = timezone(timedelta(hours=1)) # Brussels in winter (CET)
def at(day, hh, mm=0):
return datetime(2026, 11, day, hh, mm, tzinfo=L).astimezone(timezone.utc)
def st(kwh, when):
return {"state": f"{kwh}", "attributes": {"unit_of_measurement": "kWh"},
"last_updated": when.isoformat(), "last_reported": when.isoformat()}
TODAY = [("sensor.t", None)]
M = ["sensor.m1", "sensor.m2", "sensor.m3"]
def run(hold, readings, now, today_at=None):
t = [("sensor.t", st(0.5, today_at or now - timedelta(minutes=5)))]
return solarfc.read_sum(
t, [(e, st(k, w)) for e, (k, w) in zip(M, readings)], now, hold)
# The live day: m2 last reported 14:54, the others keep reporting.
h = solarfc.Hold()
run(h, [(4.0, at(6, 14, 0)), (5.0, at(6, 14, 54)), (6.0, at(6, 14, 0))],
at(6, 14, 55))
fresh = lambda w: [(4.0, w), (5.0, at(6, 14, 54)), (6.0, w)] # noqa: E731
f, why, gaps = run(h, fresh(at(6, 18, 20)), at(6, 18, 30))
check("18:30, one member last reported 14:54: tomorrow sum is there (not "
"flagged kept before 20:00)",
f is not None and f.tomorrow is not None and f.tomorrow.kwh == 15.0
and not f.kept and gaps == ())
f, _w, _g = run(h, fresh(at(6, 17, 40)), at(6, 17, 50))
check("17:50 (just under 3 h): fresh, not kept",
f is not None and f.tomorrow.kwh == 15.0 and not f.kept)
f, _w, _g = run(h, fresh(at(6, 17, 55)), at(6, 18, 0))
check("18:00 (stale since 17:54): tomorrow there",
f is not None and f.tomorrow is not None and not f.kept)
f, _w, _g = run(h, fresh(at(6, 19, 50)), at(6, 19, 55))
check("19:55 (before the bell ends): tomorrow there", f.tomorrow is not None)
# Too old: over KEPT_MAX_AGE_S (12 h) when used.
h2 = solarfc.Hold()
run(h2, [(4.0, at(6, 5, 0)), (5.0, at(6, 5, 0)), (6.0, at(6, 5, 0))],
at(6, 5, 5))
w = at(6, 16, 50)
f, why, gaps = run(h2, [(4.0, w), (5.0, at(6, 5, 0)), (6.0, w)],
at(6, 17, 10))
check("member 12 h 10 min old: no tomorrow sum (OPT-17 rule), today stays",
f is not None and f.tomorrow is None
and [g[0] for g in gaps] == ["tomorrow"])
f, _w, _g = run(h2, [(4.0, w), (5.0, at(6, 5, 0)), (6.0, w)],
at(6, 16, 50))
check("... and 11 h 50 min old is still kept",
f is not None and f.tomorrow is not None and not f.kept)
# Not a partial sum: a member never read has nothing to stand in.
f, _w, gaps = run(solarfc.Hold(), [(4.0, at(6, 18, 0)),
(5.0, at(6, 14, 54)),
(6.0, at(6, 18, 0))], at(6, 18, 30))
check("a member with no kept reading: no tomorrow sum, no partial sum",
f is not None and f.tomorrow is None and len(gaps) == 1)
# Today members are as before: a stale today member at 18:30 is not kept.
h3 = solarfc.Hold()
solarfc.read_sum([("sensor.t", st(1.0, at(6, 14, 54)))],
[(M[0], st(9.0, at(6, 14, 54)))], at(6, 14, 55), h3)
f, why, _g = solarfc.read_sum([("sensor.t", st(1.0, at(6, 14, 54)))],
[(M[0], st(9.0, at(6, 18, 20)))],
at(6, 18, 30), h3)
check("stale today member at 18:30: not kept, no forecast",
f is None and why == solarfc.STALE)
f, _w, _g = solarfc.read_sum([("sensor.t", st(1.0, at(6, 14, 54)))],
[(M[0], st(9.0, at(6, 20, 0)))],
at(6, 20, 30), h3)
check("... and at 20:30 it is kept, as before (the bell has ended)",
f is not None and f.kept)
# After midnight: a tomorrow reading from before midnight is never tomorrow's.
h4 = solarfc.Hold()
run(h4, [(4.0, at(6, 14, 0)), (5.0, at(6, 14, 0)), (6.0, at(6, 14, 0))],
at(6, 14, 5))
for label, now in (("00:30", at(7, 0, 30)), ("10:00", at(7, 10, 0)),
("15:00", at(7, 15, 0))):
f, _w, _g = solarfc.read_sum(
[("sensor.t", st(2.0, now - timedelta(minutes=5)))],
[(e, st(k, at(6, 14, 0))) for e, k in zip(M, (4.0, 5.0, 6.0))],
now, h4)
check(f"{label} on the 7th: the 6th's tomorrow figure is not the "
"7th's tomorrow", f is not None and f.tomorrow is None)
# Before 20:00 a kept tomorrow reading is not "kept for tonight": today's
# line stays the normal one (EN and NL) and the event log (main.py, from
# `fc.kept`) has no "kept".
h5 = solarfc.Hold()
solarfc.read_sum([("t", st(3.0, at(6, 9, 0)))], [("m", st(12.0, at(6, 9, 5)))],
at(6, 9, 6), h5)
for hh in (14, 18, 19):
f, _w, _g = solarfc.read_sum([("t", st(2.5, at(6, hh - 1, 55)))],
[("m", st(12.0, at(6, 9, 5)))],
at(6, hh, 0), h5)
check(f"{hh}:00, today fresh, tomorrow kept from 09:05: tomorrow there, "
"not flagged kept", f is not None and f.tomorrow is not None
and f.tomorrow.kwh == 12.0 and f.kept is False)
f, _w, _g = solarfc.read_sum([("t", st(2.5, at(6, 13, 55)))],
[("m", st(12.0, at(6, 9, 5)))], at(6, 14, 0), h5)
for lang in ("en", "nl"):
with homeview.in_lang(lang):
line = homeview.line_solar(f.today.kwh, f.today.kwh * 0.7,
homeview.hhmm(f.at), kept=f.kept)
kept_line = homeview.line_solar(1, 1, homeview.hhmm(f.at), kept=True)
print(f" {lang}: {line!r}")
if lang == "en":
check("... EN says 'Solar still to come today'",
line.startswith("Solar still to come today"))
check(f"14:00 Home line ({lang}) is the normal line, not the kept one",
line != kept_line and "2,5" in line.replace(".", ",")
and "13:55" in line)
f, _w, _g = solarfc.read_sum([("t", st(0.2, at(6, 20, 55)))],
[("m", st(12.0, at(6, 9, 5)))], at(6, 21, 0), h5)
check("21:00 as before: tomorrow kept from 09:05 is flagged kept",
f is not None and f.kept and f.tomorrow.kwh == 12.0)
print()
if fails:
print(f"{len(fails)} FAILED:")
for f in fails:
print(" -", f)
sys.exit(1)
print("all checks passed")