OPT-27 round 3: a kept figure is at most 12 h old; DST-safe midnight bound; pruning test
checks / unit (pull_request) Successful in 15m37s
infra-pin / fresh (pull_request) Successful in 1s

F1: the evening rule has an age limit (KEPT_MAX_AGE_S).
F2: the evening bound is the next local midnight (_bounds), right on 25 h and 23 h days.
F5: test that old days are dropped from the stored hold.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-30 20:41:34 +01:00
co-authored by Claude
parent 307cb47caf
commit 13b61d586a
4 changed files with 81 additions and 5 deletions
+71
View File
@@ -250,6 +250,77 @@ check("an hourly split (Solcast shape) survives the store",
fq is not None and fq.tomorrow.periods
and abs(fq.between(at(4, 8, 0), at(4, 16, 0)) - 7.5) < 1e-6)
# Round 3. F1: a kept reading is at most KEPT_MAX_AGE_S old when used.
hz = solarfc.Hold()
rs(st(0.4, at(3, 0, 5)), st(12.0, at(3, 0, 5)), at(3, 0, 10), hz)
print(f" 00:05 reading, sensors dead: 19:59 "
f"{rs(GONE, GONE, at(3, 19, 59), hz)[0]}, 20:00 "
f"{rs(GONE, GONE, at(3, 20, 0), hz)[0]}")
check("F1: a 00:05 reading with the sensors dead all day is not kept at "
"20:00 (20 h old): no forecast",
rs(GONE, GONE, at(3, 20, 0), hz)[0] is None
and rs(GONE, GONE, at(3, 23, 0), hz)[0] is None)
check("... nor after midnight until 03:00 (as today's, 26 h old)",
rs(GONE, GONE, at(4, 0, 30), hz)[0] is None
and rs(GONE, GONE, at(4, 2, 30), hz)[0] is None)
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 ...",
(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)
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",
rs(GONE, GONE, at(3, 20, 0), hm)[0] is not None
and rs(GONE, GONE, at(3, 22, 0), hm)[0] is None)
# F2: the 25 h autumn day (2026-10-25) and the 23 h spring day (2027-03-28).
BRU = solarfc.BILLING_TZ # Europe/Brussels, or the encoded EU rule
def bru(y, mo, d, hh, mm=0):
return datetime(y, mo, d, hh, mm, tzinfo=BRU).astimezone(timezone.utc)
for (y, mo, d), label in (((2026, 10, 25), "25 h autumn day 2026-10-25"),
((2027, 3, 28), "23 h spring day 2027-03-28")):
hd = solarfc.Hold()
t17 = bru(y, mo, d, 17, 5)
rs(st(0.4, t17), st(12.0, t17), t17 + timedelta(minutes=5), hd)
# real hours from 20:00 local to the next local midnight, each hour
start = bru(y, mo, d, 20, 0)
end = bru(y, mo, d + 1, 0)
t, miss = start, []
while t < end:
if rs(GONE, GONE, t, hd)[0] is None:
miss.append(t.astimezone(BRU).strftime("%H:%M"))
t += timedelta(minutes=30)
print(f" {label}: not kept at {miss}")
check(f"F2: {label}: the 17:05 figure is kept every half hour "
"from 20:00 to local midnight", not miss)
check(f"... and just before the next local midnight",
rs(GONE, GONE, bru(y, mo, d + 1, 0) - timedelta(minutes=1),
hd)[0] is not None)
# F5: old days are dropped from the stored hold, so it does not grow.
hp5 = solarfc.Hold()
sizes = []
for dd in range(3, 13):
t0_ = at(dd, 17, 5)
rs(st(0.4, t0_), st(12.0, t0_), t0_ + timedelta(minutes=5), hp5)
sizes.append(len(hp5.dump()["r"]))
day0s = {row[2] for row in hp5.dump()["r"]}
print(f" F5: stored entries per day over 10 days: {sizes}")
check("F5: the stored hold does not grow day after day (at most today's and "
"tomorrow's entry, with yesterday's tomorrow)", max(sizes) <= 3
and len(day0s) <= 2)
check("... and a day before today is gone",
all(row[2] >= at(12, 0, 0).isoformat() for row in hp5.dump()["r"]))
# --------------------------------------------------------------------------- #
print("\n2. The real Controller: day/night 0.34 / 0.26, cycle cost 0.07")
A, B = "inverter-a", "inverter-b"