OPT-11 P4 review: Cheap hours ignores the plan; midnight-stale forecast; stricter ids

- Cheap hours only fills to the ceiling with or without a forecast, as the
  design (section 1) says. Skipping the sun's share cost 0.31 EUR on real
  2026 prices (29.72 against P3's 29.41) and saved nothing on the P3 set.
- A forecast reading from before local midnight is stale: until the
  integration refreshes, "left today" is still yesterday's 0 and
  "tomorrow" is really today.
- Forecast entity ids must be sensor.[a-z0-9_]+ (they go into the
  /api/states/<id> path).
- House profile: a stored day that is not a date, or is after today, is
  dropped instead of kept forever.
- Text: the forecast line says "for the rest of today"; the learning line
  no longer promises "tomorrow" (a plan needs one full day).
- Tests for four surviving review mutants (hour sample floor, the dear run
  after the cheap run, share by room, daytime use) and the fixes above.
- Replay tool: `--fc zero` (a forecast of no sun).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-09-29 22:08:47 +02:00
co-authored by Claude Opus 5.5
parent aabc2a27fd
commit fbd884797d
10 changed files with 127 additions and 56 deletions
+11 -10
View File
@@ -24,10 +24,10 @@ Roles (the page's Why line names them):
Rules that keep this safe (tests: test_modes_p3.py):
* every range holds 0 W (a mode may forbid a direction, never force one);
* a Solar first battery never charges from the grid;
* P4: with a day plan (`targets`), a Leftover solar or Cheap hours battery
charges from the grid only up to its planned level, never above the
ceiling; with none, Cheap hours fills to the ceiling and Leftover solar
takes solar only (P3 option A, `charge_to()`);
* P4: with a day plan (`targets`), a Leftover solar battery charges from
the grid only up to its planned level, never above the ceiling; with
none it takes solar only (P3 option A). Cheap hours only fills to the
ceiling either way (`charge_to()`);
* grid charging needs a measured peak guard that is clear and a measured
headroom (SAFETY-45); the trade is sized to that headroom, whose aim is
`peak_cap_w - peak_margin_w` (SAFETY-79). It starts only at START_W or
@@ -98,12 +98,13 @@ def charge_to(p: Pack, ceiling_pct, targets=None):
`targets` is the day plan (`dayplan.plan()`: name -> level), or None
when there is none (no forecast, stale, or not configured). With no plan
this is P3 option A exactly: Cheap hours to the ceiling, the others never.
With a plan, a Leftover solar or Cheap hours battery charges only to its
planned level (the sun fills the rest), and a battery the plan leaves out
falls back to the no-plan answer. Solar first never charges."""
if targets is None or p.name not in targets:
return float(ceiling_pct) if p.mode in GRID_CHARGE_MODES else None
if p.mode not in PLAN_CHARGE_MODES:
With a plan, a Leftover solar battery charges only to its planned level
(the sun fills the rest); a battery the plan leaves out, Cheap hours only
included, gets the no-plan answer. Solar first never charges."""
if p.mode in GRID_CHARGE_MODES:
return float(ceiling_pct)
if targets is None or p.name not in targets \
or p.mode not in PLAN_CHARGE_MODES:
return None
t = targets[p.name]
return None if t is None else min(float(ceiling_pct), float(t))