OPT-27 round 3: a kept figure is at most 12 h old; DST-safe midnight bound; pruning test
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:
co-authored by
Claude
parent
307cb47caf
commit
13b61d586a
@@ -26,7 +26,7 @@ sensor that is missing or stale, never one with a bad value or unit, and:
|
||||
until midnight + 3 h (all sensors of the sum are needed, as for any sum);
|
||||
- until the next fresh reading, which replaces it at once.
|
||||
|
||||
So a figure is never used for another day and never older than one night.
|
||||
So a figure is never used for another day and never older than 12 hours (`KEPT_MAX_AGE_S`): a 17:05 reading serves until 03:00 (9 h 55 min), a 09:05 reading only until 21:05.
|
||||
Daytime is as before: a sensor that stopped is stale after 3 h, and no forecast
|
||||
all day is still no forecast (the night run still needs 1.5x).
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ With battery modes on, these choices replace the *Charging and selling on price*
|
||||
- The plan is checked every 15 minutes. A planned level moves only by 1 kWh or 10 %, so a wobbling forecast does not flip it.
|
||||
- No forecast, one older than 3 hours or from before midnight, or no house use yet: *Leftover solar* takes solar only and *Cheap hours only* fills to the top level. The Batteries screen says so.
|
||||
- A battery needs its capacity (`inverters.capacity_kwh`) for a plan.
|
||||
- **At night.** Some forecasts stop updating in the evening (Forecast.Solar does). The add-on keeps the last reading of each sensor and uses it for that night's plan: from 20:00 until 03:00, or until a newer reading comes. A sensor that is missing for up to 3 hours is covered the same way. A figure is never used for another day, and never older than that night. The Batteries screen says "Solar forecast read 17:05, kept for tonight." With no reading all day, the night run still needs the wider gap.
|
||||
- **At night.** Some forecasts stop updating in the evening (Forecast.Solar does). The add-on keeps the last reading of each sensor and uses it for that night's plan: from 20:00 until 03:00, or until a newer reading comes. A sensor that is missing for up to 3 hours is covered the same way. A figure is never used for another day, and never when it is more than 12 hours old. The Batteries screen says "Solar forecast read 17:05, kept for tonight." With no reading all day, the night run still needs the wider gap.
|
||||
|
||||
**What always comes first.**
|
||||
- The peak limit, the safety stops and the monthly cycle come before a battery's mode.
|
||||
|
||||
@@ -64,6 +64,9 @@ BELL_FROM_H, BELL_TO_H = 6.0, 20.0
|
||||
# OPT-27: a kept reading serves the night plan from the end of the bell until
|
||||
# this many hours after local midnight (the card: "local midnight + 3 h").
|
||||
KEPT_UNTIL_H = 3.0
|
||||
# OPT-27: and a reading older than this when used is not kept (a morning
|
||||
# figure must not drive the night).
|
||||
KEPT_MAX_AGE_S = 12 * 3600.0
|
||||
SAVE_EVERY_S = 900.0
|
||||
|
||||
# Why there is no forecast (the page words are in ui_text `home_words`).
|
||||
@@ -318,7 +321,8 @@ 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,
|
||||
or within `KEPT_UNTIL_H` after it. `kept` is true when (b) was needed,
|
||||
or within `KEPT_UNTIL_H` after it, and then at most `KEPT_MAX_AGE_S`
|
||||
old. `kept` is true when (b) was needed,
|
||||
or `as_today` (a tomorrow figure standing in for today, after
|
||||
midnight)."""
|
||||
rec = self._r.get((kind, entity, day0))
|
||||
@@ -327,11 +331,12 @@ class Hold:
|
||||
day, at = rec
|
||||
if (now - at).total_seconds() <= FRESH_S and not as_today:
|
||||
return day, at, False
|
||||
if (now - at).total_seconds() > KEPT_MAX_AGE_S:
|
||||
return None
|
||||
# Both sides: a tomorrow sensor is asked with day0 = the next
|
||||
# midnight, which is in the future all day (round 2).
|
||||
after_midnight = day0 <= now < day0 + timedelta(hours=KEPT_UNTIL_H)
|
||||
evening = _local_hour(now) >= BELL_TO_H and now < day0 + timedelta(
|
||||
days=1)
|
||||
evening = _local_hour(now) >= BELL_TO_H and now < _bounds(day0)[1]
|
||||
if as_today:
|
||||
return (day, at, True) if after_midnight else None
|
||||
return (day, at, True) if (evening or after_midnight) else None
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user