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>
160 lines
6.7 KiB
Python
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")
|