OPT-34 fix (rig F-1): a replaced evening forecast does not come back
checks / unit (pull_request) Successful in 21m5s
infra-pin / fresh (pull_request) Successful in 2s

After a fresh today reading in the night (02:00 on the rig) went stale
(05:00), Hold.stand_in(as_today=True) served the 17:05 "tomorrow" figure
again until 06:00: the plan went back to 95 %, the latch took it, and
Leftover solar charged back up. read_sum() now skips that stand-in once
every today sensor has given a fresh reading for the day
(Hold.replaced()); the stale newer reading follows the OPT-27 rules.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-01 12:26:05 +01:00
co-authored by Claude
parent d4c01daca5
commit 6263a57b6b
4 changed files with 117 additions and 5 deletions
+13 -3
View File
@@ -38,7 +38,8 @@ midnight, if it is at most `KEPT_MAX_AGE_S` old when used.
Never beyond that, and never for another day: a today figure is only today's,
a tomorrow figure is tomorrow's until midnight and then, until the bell starts
(`BELL_FROM_H`, 06:00 local), today's (OPT-34: at most `KEPT_MORNING_MAX_AGE_S`
old, and it must have been usable at midnight).
old, it must have been usable at midnight, and no fresh today reading may
have replaced it since).
DECISIONS/OPT-27-last-fresh-forecast-for-the-night.md,
DECISIONS/OPT-34-the-night-charge-keeps-its-level.md.
@@ -351,6 +352,13 @@ class Hold:
evening = _local_hour(now) >= BELL_TO_H and now < _bounds(day0)[1]
return (day, at, True) if evening else None
def replaced(self, entities, day0) -> bool:
"""OPT-34 F-1: every today sensor has given a fresh reading for
`day0` (so after midnight, newer than any evening figure). The
evening's tomorrow figure is then no stand-in for today any more,
also once those readings are stale."""
return all(("today", e, day0) in self._r for e in entities)
def dump(self) -> dict:
return {"v": 1, "r": [
[kind, ent, d.day0.isoformat(), d.kwh, at.isoformat(),
@@ -492,7 +500,8 @@ def read_sum(today, tomorrow, now: datetime, hold: Hold | None = None):
where `Hold.stand_in()` allows it (the forecast then has `kept` set when
that took a reading older than `FRESH_S`). After midnight, if the today
side is still short, the tomorrow figures read before midnight are
today's, until the bell starts (`stand_in()`). Without a `hold` this is
today's, until the bell starts (`stand_in()`), unless fresh today
readings have replaced them (`Hold.replaced()`). Without a `hold` this is
exactly as before."""
if not today:
return None, NOT_SET, ()
@@ -537,7 +546,8 @@ def read_sum(today, tomorrow, now: datetime, hold: Hold | None = None):
# day; all of them are needed, as for any sum.
subs = [hold.stand_in("tomorrow", ent, day0, now, as_today=True)
for ent, _st in tomorrow]
if all(r is not None for r in subs):
if all(r is not None for r in subs) and not hold.replaced(
[ent for ent, _st in today], day0):
days = [r[0] for r in subs]
ats = [r[1] for r in subs]
gaps = [g for g in gaps if g[0] != "today"]