From 0bacafadf04f814efbae7702d9b3fa3750a1121d Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sat, 7 Nov 2026 06:02:00 +0100 Subject: [PATCH] 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 --- goodwe_controller/app/solarfc.py | 10 ++++--- goodwe_controller/test_opt27.py | 2 +- goodwe_controller/test_opt34.py | 4 +-- goodwe_controller/test_opt38.py | 45 +++++++++++++++++++++++++++----- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/goodwe_controller/app/solarfc.py b/goodwe_controller/app/solarfc.py index 3cda6dc..6a4939e 100644 --- a/goodwe_controller/app/solarfc.py +++ b/goodwe_controller/app/solarfc.py @@ -332,8 +332,9 @@ class Hold: The kept reading must be for `day0`, and either (a) at most `FRESH_S` old, or (b) used at night: from the end of the bell to local midnight, - at most `KEPT_MAX_AGE_S` old. `kept` is true when (b) was needed, - or `as_today` (OPT-34: a tomorrow figure standing in for today, after + at most `KEPT_MAX_AGE_S` old; a "tomorrow" reading at any hour + (OPT-38; `read_sum()` flags it kept only from the end of the bell). + `kept` is true when (b) was needed, or `as_today` (OPT-34: a tomorrow figure standing in for today, after midnight and until the bell starts, `KEPT_MORNING_MAX_AGE_S`).""" rec = self._r.get((kind, entity, day0)) if rec is None: @@ -558,6 +559,9 @@ def read_sum(today, tomorrow, now: datetime, hold: Hold | None = None): if why0 is not None: return None, why0, gaps tday = _sum(tdays) if tomorrow and len(tdays) == len(tomorrow) else None - kept = kept or (tday is not None and tkept) + # OPT-38: before the bell ends a kept tomorrow reading is no "kept for + # tonight": it must not replace today's line or the log. + kept = kept or (tday is not None and tkept + and _local_hour(now) >= BELL_TO_H) # The oldest member's report is when the sum was last fully known. return Forecast(_sum(days), tday, min(ats), kept), None, gaps diff --git a/goodwe_controller/test_opt27.py b/goodwe_controller/test_opt27.py index 1d0e7a8..7f3809d 100644 --- a/goodwe_controller/test_opt27.py +++ b/goodwe_controller/test_opt27.py @@ -146,7 +146,7 @@ for label, tm in (("stale", st(12.0, at(4, 9, 5))), ("unavailable", GONE)): check(f"14:00, today fresh, tomorrow {label} since 09:05 (over 3 h): " "the 09:05 figure covers it, kept (OPT-38: at any hour)", fd is not None and fd.tomorrow is not None - and fd.tomorrow.kwh == 12.0 and fd.kept) + and fd.tomorrow.kwh == 12.0 and not fd.kept) fn = rs(st(0.1, at(4, 20, 55)), tm, at(4, 21, 0), h9)[0] check(f"21:00, tomorrow {label}: the 09:05 figure covers it, kept", fn is not None and fn.tomorrow is not None diff --git a/goodwe_controller/test_opt34.py b/goodwe_controller/test_opt34.py index 6165010..fdb4698 100644 --- a/goodwe_controller/test_opt34.py +++ b/goodwe_controller/test_opt34.py @@ -186,9 +186,9 @@ check("a fresh figure replaces the kept one at once (04:00: 5.0 today, " and fy.today.kwh == 5.0 and fy.tomorrow.kwh == 9.0) fy2 = rs(st(5.0, at(4, 7, 55)), GONE, at(4, 8, 0), hd)[0] check("... and at 08:00, today fresh and tomorrow's sensor down for over 3 h: " - "tomorrow is the 03:50 figure for the 5th (9.0, kept: OPT-38), never " + "tomorrow is the 03:50 figure for the 5th (9.0, not flagged kept before 20:00: OPT-38), never " "the 3rd's 12.0", fy2 is not None and fy2.today.kwh == 5.0 - and fy2.tomorrow is not None and fy2.tomorrow.kwh == 9.0 and fy2.kept) + and fy2.tomorrow is not None and fy2.tomorrow.kwh == 9.0 and not fy2.kept) # Restart: the saved hold keeps the figure. for label, now in (("01:00", at(4, 1, 0)), ("04:00", at(4, 4, 0))): diff --git a/goodwe_controller/test_opt38.py b/goodwe_controller/test_opt38.py index d4ef310..11ce381 100644 --- a/goodwe_controller/test_opt38.py +++ b/goodwe_controller/test_opt38.py @@ -16,7 +16,7 @@ from datetime import datetime, timedelta, timezone HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) -from app import solarfc # noqa: E402 +from app import homeview, solarfc # noqa: E402 fails = [] @@ -55,15 +55,16 @@ 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, kept", +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 f.kept and gaps == ()) + 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): kept, tomorrow there", - f is not None and f.tomorrow is not None and f.kept) +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) @@ -80,7 +81,7 @@ check("member 12 h 10 min old: no tomorrow sum (OPT-17 rule), today stays", 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 f.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)), @@ -117,6 +118,38 @@ for label, now in (("00:30", at(7, 0, 30)), ("10:00", at(7, 10, 0)), 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:")