OPT-34: the night charge keeps its level when the kept forecast ends
checks / unit (pull_request) Successful in 18m59s
infra-pin / fresh (pull_request) Successful in 1s

With the forecast sensors silent from 17:05 the kept evening figure ended at
midnight + 3 h, in the middle of the night grid charge, and Leftover solar's
planned level went with it (rig: A 49.8 % against 79.9 %).

- A night grid charge that has started keeps its planned level when the forecast
  goes (Controller._latch_charge, store key night_charge_latch), and the narrow
  price gap with it (weather_held). A fresh figure still moves the level.
- The evening's tomorrow figure is today's until the bell starts (06:00 local),
  up to 15 h old but only if it was usable at midnight (solarfc.Hold.stand_in).
- Screen: "No solar forecast now. The charge under way goes on to its planned
  level." (EN/NL) while a latched battery is below its level.
- test_opt34.py (whole nights through the real control cycle, mutants), test_opt27
  and test_modes_p4 adjusted to the new window and the kept level.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-31 17:00:31 +01:00
co-authored by Claude
parent deee98d352
commit 412e47f111
12 changed files with 783 additions and 64 deletions
+27 -18
View File
@@ -7,9 +7,10 @@ Forecast.Solar site was "no forecast": a price mode's night cheap run needed
fresh reading of each sensor, for the day it is FOR:
* up to `FRESH_S` when the sensor is missing or unavailable (a short outage);
* from the end of the bell (20:00) until local midnight + 3 h, if the reading
is at most `KEPT_MAX_AGE_S` (12 h) old when used (the night plan); never
beyond, never for another day.
* from the end of the bell (20:00) until local midnight, if the reading is at
most `KEPT_MAX_AGE_S` (12 h) old when used (the night plan); after midnight
the evening's tomorrow figure is today's until the bell starts (06:00, OPT-34,
test_opt34.py); never beyond, never for another day.
DECISIONS/OPT-27-last-fresh-forecast-for-the-night.md.
@@ -21,7 +22,7 @@ DECISIONS/OPT-27-last-fresh-forecast-for-the-night.md.
b. no forecast all day: the night run still needs 1.5x and does not charge;
c. an evening restart keeps it (a new Controller on the saved store);
d. a one-hour outage at 18:47 does not drop it;
e. past midnight + 3 h the figure is not used, before it it is.
e. from 06:00 (the bell) the figure is not used, before it it is (OPT-34).
3. The screens: the forecast line says when it was read and that it is kept
for tonight (EN and NL), and Home agrees with control.
@@ -153,7 +154,7 @@ for label, tm in (("stale", st(12.0, at(4, 9, 5))), ("unavailable", GONE)):
check(f"11:00, tomorrow {label} for under 3 h: still covered, not kept",
fs is not None and fs.tomorrow is not None and not fs.kept)
# The bound: midnight + 3 h, and the day a figure is for.
# The bound: the bell start (06:00), and the day a figure is for.
fm = rs(st(0.0, at(3, 23, 0)), st(0.0, at(3, 23, 0)), at(4, 1, 0), h2)[0]
check("00:30 is covered by the 18:05 tomorrow figure, as TODAY's",
rs(GONE, GONE, at(4, 0, 30), h2)[0] is not None)
@@ -165,9 +166,12 @@ check("02:59: tomorrow's 12.0 kWh is now today's, there is no tomorrow",
check("... its sun is counted on the day it is for (06:00-20:00 on the 4th)",
fmid is not None and fmid.between(at(4, 3, 0), at(4, 7, 0)) is not None
and fmid.between(at(4, 3, 0), at(4, 7, 0)) > 0.0)
check("03:01 (midnight + 3 h): not used any more",
rs(GONE, GONE, at(4, 3, 1), h2)[0] is None)
check("... and not the next evening either (past midnight + 3 h)",
check("03:01: still used (OPT-34: until the bell starts at 06:00)",
rs(GONE, GONE, at(4, 3, 1), h2)[0] is not None
and rs(GONE, GONE, at(4, 5, 59), h2)[0] is not None)
check("06:00 (the bell starts): not used any more",
rs(GONE, GONE, at(4, 6, 0), h2)[0] is None)
check("... and not the next evening either (past 06:00)",
rs(GONE, GONE, at(4, 22, 0), h2)[0] is None)
fe = rs(st(0.1, at(4, 21, 50)), GONE, at(4, 22, 0), h2)[0]
check("... the next evening, with today fresh and tomorrow's sensor down, "
@@ -268,11 +272,16 @@ hy = solarfc.Hold()
rs(st(0.4, at(3, 17, 5)), st(12.0, at(3, 17, 5)), at(3, 17, 10), hy)
check("F1: a 17:05 reading is kept at 22:00 (5 h) ...",
(lambda f: f is not None and f.kept)(rs(GONE, GONE, at(3, 22, 0), hy)[0]))
check("... and at 02:59 next day (9 h 54 min), the midnight + 3 h bound ...",
check("... and at 02:59 next day (9 h 54 min) ...",
(lambda f: f is not None and f.kept)(rs(GONE, GONE, at(4, 2, 59),
hy)[0]))
check("... and at 03:00 next day it is not (midnight + 3 h; 9 h 55 min old)",
rs(GONE, GONE, at(4, 3, 0), hy)[0] is None)
check("... and at 03:00 and 05:59 next day (OPT-34: the bell starts at 06:00)",
(lambda f: f is not None and f.kept)(rs(GONE, GONE, at(4, 3, 0),
hy)[0])
and (lambda f: f is not None and f.kept)(rs(GONE, GONE, at(4, 5, 59),
hy)[0]))
check("... and at 06:00 it is not (12 h 55 min old, the bell has started)",
rs(GONE, GONE, at(4, 6, 0), hy)[0] is None)
hm = solarfc.Hold()
rs(st(0.9, at(3, 9, 5)), st(12.0, at(3, 9, 5)), at(3, 9, 10), hm)
check("F1: a 09:05 reading is kept at 20:00 (10 h 55 min) but not at 22:00",
@@ -564,18 +573,18 @@ check("... and the 22:00 night run is planned and charges",
present(ld) and ld["runs_on"] is True
and sum(r["charging"] for r in rd) > len(rd) // 2)
# e. the bound: midnight + 3 h
# e. the bound: until the bell starts (06:00, OPT-34; it was midnight + 3 h)
mid = [at(DAY3, 16, 0) + timedelta(minutes=30 * i) for i in range(12)]
ce, re_, pe = drive27(opts(), stops_at(LAST), pre=mid + [at(4, 2, 30)],
start=at(4, 2, 30))
ce, re_, pe = drive27(opts(), stops_at(LAST), pre=mid + [at(4, 5, 30)],
start=at(4, 5, 30))
le = nth(re_, -1, "last cycle")
check("02:30 (before midnight + 3 h): the figure from 17:05 is still used, "
check("05:30 (before the bell starts): the figure from 17:05 is still used, "
"as today's; the night run is planned",
present(le) and le["kept"] and le["runs_on"] is True)
cf, rf, pf = drive27(opts(), stops_at(LAST), pre=mid + [at(4, 3, 30)],
start=at(4, 3, 30))
cf, rf, pf = drive27(opts(), stops_at(LAST), pre=mid + [at(4, 6, 30)],
start=at(4, 6, 30))
lf = nth(rf, -1, "last cycle")
check("03:30 (after midnight + 3 h): a figure from before the night is not "
check("06:30 (the bell has started): a figure from before the night is not "
"used: no forecast, 1.5x, not worth, no charge",
present(lf) and lf["fc_none"] and lf["runs_on"] is False
and lf["reason"] == "not_worth" and not any(r["charging"] for r in rf))