OPT-27: the night cheap run is judged with the last fresh forecast #219

Merged
admin merged 7 commits from OPT-27 into release/1.0 2026-10-30 22:16:33 +01:00
Owner

OPT-27: the night cheap run is judged with the last fresh forecast, not as "no forecast".

Read this first: the base. release/1.0 (4a1f6cc) does not contain TARIFF-24 (it is in hotfix-0.19.1, then master 2932f3a). OPT-27 is about that code (the 1.5x widening, weather_held), so this branch starts with a clean merge of master 2932f3a into release/1.0 (commit 7433786, no conflicts), then two commits of their own:

  • 6656535 tools/ux54_scenarios.py: with master in, test_ux54.py failed 21 checks (a forecast now replaces the 1.5x, so its scenarios planned more give blocks). The scenarios now set cycle cost 0.15 where they have a forecast, the same gate as before. This is the kind of fix the planned master -> release/1.0 merge needs anyway; the reviewer may prefer to take it out of this PR once that merge lands.
  • cdcbe7d and 1eb572f: OPT-27 itself.

The defect. Forecast.Solar's sensors stop changing in the evening, so at night every such site had no forecast (stale after 3 h): the night cheap run needed 1.5x the cycle cost and Leftover solar planned nothing. One missed poll (unavailable) also dropped the forecast at once.

The change (DECISIONS/OPT-27-last-fresh-forecast-for-the-night.md). solarfc.Hold keeps the last fresh reading of each sensor, for the day it is for. A missing or stale sensor is covered by it (a) for up to 3 h (a short outage, any time of day) and (b) from 20:00 until local midnight + 3 h, however old. A tomorrow figure is tomorrow's until midnight and then today's; a today figure is only today's. Never another day, never older than one night; the next fresh reading replaces it. It is stored in the controller's store (solar_fc_hold), so an evening restart keeps it. solar_fc stays the one forecast every reader uses (plan, gate, Home, Forecast tab), so control and screens agree. Daytime is as before; no forecast all day still means 1.5x.

Screens. "Solar forecast read 17:05, used for tonight's plan." / "Zonneprognose van 17:05, gebruikt voor het plan van vanavond."

Tests. test_opt27.py (new): the solarfc hold and its bounds, and the real Controller over a day whose forecast stops at 17:05: the 22:00 run is planned with the cycle-cost gap and charges; no forecast all day still needs 1.5x; an evening restart keeps it; a one-hour outage at 18:47 does not drop it; at 03:30 a figure from the evening is not used. Two existing tests changed: test_tariff24_weather.py and test_modes_p4.py test "the forecast is lost", so they now lose it with a hold that never covers (and test_modes_p4.py gains the covered case).

Digests (command digests, base = the merge commit 7433786): identical for modes off, arbitrage on, modes on with a fresh forecast, modes on with no forecast, and with an unavailable forecast. They change only for a forecast that stops at 17:05 (2 of 4 cases); with the hold turned off the head reproduces base. Details in reports/OPT-27-dev-report.md.

No version bump. The CHANGELOG has no unreleased section at its top, so there is no CHANGELOG line.

Clash risk with OPT-23+28 (dayplan.py, solarfc.Forecast.between): in solarfc.py the Forecast dataclass gains a last field kept (just above between); everything else is new code (Hold, _COVERABLE) or in read_sum()/_tomorrow(). dayview.py and homeview.py: one call each to line_solar(..., kept=).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

OPT-27: the night cheap run is judged with the last fresh forecast, not as "no forecast". **Read this first: the base.** `release/1.0` (4a1f6cc) does not contain TARIFF-24 (it is in `hotfix-0.19.1`, then `master` 2932f3a). OPT-27 is about that code (the 1.5x widening, `weather_held`), so this branch starts with a clean merge of `master` 2932f3a into `release/1.0` (commit 7433786, no conflicts), then two commits of their own: - `6656535` `tools/ux54_scenarios.py`: with master in, `test_ux54.py` failed 21 checks (a forecast now replaces the 1.5x, so its scenarios planned more give blocks). The scenarios now set cycle cost 0.15 where they have a forecast, the same gate as before. This is the kind of fix the planned master -> release/1.0 merge needs anyway; the reviewer may prefer to take it out of this PR once that merge lands. - `cdcbe7d` and `1eb572f`: OPT-27 itself. **The defect.** Forecast.Solar's sensors stop changing in the evening, so at night every such site had no forecast (stale after 3 h): the night cheap run needed 1.5x the cycle cost and Leftover solar planned nothing. One missed poll (`unavailable`) also dropped the forecast at once. **The change** (DECISIONS/OPT-27-last-fresh-forecast-for-the-night.md). `solarfc.Hold` keeps the last fresh reading of each sensor, for the day it is for. A missing or stale sensor is covered by it (a) for up to 3 h (a short outage, any time of day) and (b) from 20:00 until local midnight + 3 h, however old. A tomorrow figure is tomorrow's until midnight and then today's; a today figure is only today's. Never another day, never older than one night; the next fresh reading replaces it. It is stored in the controller's store (`solar_fc_hold`), so an evening restart keeps it. `solar_fc` stays the one forecast every reader uses (plan, gate, Home, Forecast tab), so control and screens agree. Daytime is as before; no forecast all day still means 1.5x. **Screens.** "Solar forecast read 17:05, used for tonight's plan." / "Zonneprognose van 17:05, gebruikt voor het plan van vanavond." **Tests.** `test_opt27.py` (new): the solarfc hold and its bounds, and the real Controller over a day whose forecast stops at 17:05: the 22:00 run is planned with the cycle-cost gap and charges; no forecast all day still needs 1.5x; an evening restart keeps it; a one-hour outage at 18:47 does not drop it; at 03:30 a figure from the evening is not used. Two existing tests changed: `test_tariff24_weather.py` and `test_modes_p4.py` test "the forecast is lost", so they now lose it with a hold that never covers (and `test_modes_p4.py` gains the covered case). **Digests** (command digests, base = the merge commit 7433786): identical for modes off, arbitrage on, modes on with a fresh forecast, modes on with no forecast, and with an unavailable forecast. They change only for a forecast that stops at 17:05 (2 of 4 cases); with the hold turned off the head reproduces base. Details in `reports/OPT-27-dev-report.md`. No version bump. The CHANGELOG has no unreleased section at its top, so there is no CHANGELOG line. Clash risk with OPT-23+28 (`dayplan.py`, `solarfc.Forecast.between`): in `solarfc.py` the `Forecast` dataclass gains a last field `kept` (just above `between`); everything else is new code (`Hold`, `_COVERABLE`) or in `read_sum()`/`_tomorrow()`. `dayview.py` and `homeview.py`: one call each to `line_solar(..., kept=)`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 4 commits 2026-10-30 19:14:20 +01:00
Forecast.Solar's sensors stop updating in the evening, so at night every such
site had no forecast: the night cheap run needed 1.5x the cycle cost and
Leftover solar planned nothing. One missed poll also dropped it at once.

solarfc.Hold keeps the last fresh reading of each sensor, for the day it is
for. A missing or stale sensor is covered by it for up to FRESH_S (a short
outage), and from the end of the bell (20:00) until local midnight + 3 h
however old. A tomorrow figure is tomorrow's until midnight and then today's;
a today figure is only today's. Stored in the controller's store
(solar_fc_hold), so an evening restart keeps it. The forecast line says
'Solar forecast read 17:05, used for tonight's plan.' (EN, NL).

test_tariff24_weather.py runs with a hold that never covers: it tests what the
plan does when the forecast IS lost.

Co-Authored-By: Claude <noreply@anthropic.com>
test_modes_p4.py: 'the forecast is lost mid-run' now loses it for good (a hold
that never covers); a second scenario is the same loss with the hold, which
keeps Leftover solar charging. test_opt27.py: a failing check no longer hides
the ones after it.

Co-Authored-By: Claude <noreply@anthropic.com>
Hold.stand_in(): day0 <= now < day0 + 3 h. A tomorrow sensor is asked with
day0 = the next midnight, so the upper bound alone was true all day and a
figure stood in at any hour. test_opt27.py covers daytime tomorrow outages.

Co-Authored-By: Claude <noreply@anthropic.com>
OPT-27: a kept figure sizes the night under OPT-28's shortfall, as a fresh one does
checks / unit (pull_request) Successful in 17m22s
infra-pin / fresh (pull_request) Successful in 1s
60aa7f310c
Co-Authored-By: Claude <noreply@anthropic.com>
admin force-pushed OPT-27 from f095ca9268 to 60aa7f310c 2026-10-30 19:14:20 +01:00 Compare
admin added 1 commit 2026-10-30 20:11:17 +01:00
OPT-27 review: the kept forecast line says "kept for tonight"
checks / unit (pull_request) Successful in 17m20s
infra-pin / fresh (pull_request) Successful in 1s
307cb47caf
With battery modes off the forecast is still read and shown (Settings,
Simple, and the Forecast tab), but no plan uses it, so "used for tonight's
plan" was not true there (UX-53). "Kept for tonight" is true in every
shape. NL "vanavond" read wrong after midnight; "vannacht" covers the
whole night. DECISIONS' "modes off: nothing reads the sensors" corrected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Author
Owner

OPT-27 review (Opus), head 60aa7f3 → 307cb47 (text only). Verdict: changes requested, not merged.

The fix works for the case on the card: with sensors stuck at 17:05, the 22:00 night run is planned with the cycle-cost gap. The kept figure is counted once. The OPT-28 shortfall equals the fresh-figure case. Modes off, arbitrage, hourly, fresh state-only, no forecast, unavailable forecast and N=1 replay unchanged (17 digests). Local suite: 91/0. CI on 60aa7f3 (1455/1456) and 307cb47 (1457/1458) is green. 6 of 7 mutations were caught.

# Sev Where Finding
F1 Medium app/solarfc.py:333-337 (Hold.stand_in, the evening branch) The evening branch has no age limit. A reading taken at 00:05 on day D (sensors dead after that) comes back at 20:00 on D with kept=True and runs that night's plan. As today's figure, it stays in use until 03:00 on D+1, so it is up to 27 h old. Probe: 10:00/19:59 none, 20:00/23:00/02:30 kept "read 00:05", 03:01 none. This breaks the brief's "never use a figure older than one night", and DOCS.md:560 / DECISIONS:29 say "never older than that night". Behaviour fix needed: for example, branch (b) only for a reading at most ~12 h old (a 17:05 or 16:00 reading still reaches 03:00). Add a test for it.
F2 Low app/solarfc.py:333-334 now < day0 + timedelta(days=1) is not the next local midnight on a DST day. On the 25 h autumn Sunday (e.g. 2027-10-31), the kept today figure is dropped from 23:00 to 24:00 local: "none (stale)", no day plan for that hour. Use _bounds(now)[1]. Spring is fine. One-line fix, same round as F1.
F3 Low (text, fixed in 307cb47) ui_text/en.json:363, nl.json:303 With modes off the forecast is still read and the line shows in Settings › Simple and the Forecast tab. "used for tonight's plan" was false there (UX-53). NL "vanavond" was wrong after midnight. Now: "Solar forecast read {at}, kept for tonight." / "Zonneprognose van {at}, bewaard voor vannacht." DECISIONS "Modes off (nothing reads the sensors)" is corrected.
F4 Low app/solarfc.py:333 (BELL_TO_H = 20:00) If a today sensor stops before 17:00 (winter, or sites further north), there is a gap from the last change + 3 h until 20:00 with no forecast (probe: stopped 16:00 → none 19:01-19:59). Card sketch.
F5 Low (test) test_opt27.py Mutation "never forget_before" survives. Lookups are by day, so it is harmless to control, but the solar_fc_hold store entry then grows every day. No test checks the pruning.
F6 Info app/solarfc.py:328 A daytime outage is now covered for up to 3 h. A state-only figure is then counted from now, not from at (OPT-30 / F2 of the OPT-23-28 review). Before, an unavailable sensor gave no forecast. OPT-30 should include this case.
F7 Info test_opt27.py part g Part g drives the real control cycle (read_inputs + cycle, 20 min at 22:00) and compares kept with fresh exactly. At 22:00 today's state-only figure counts 0 either way, so "counted from when taken" is only checked after midnight by a solarfc-level probe (kept-as-today = fresh: 0.150 / 12.000 / 4.691 kWh).

Check 4 (TARIFF-24): a kept figure makes solar_fc not None, so the narrow gap applies in control and in the Forecast tab (1c9fefd) alike. That is the card's intent: the run is judged with the forecast that was fresh when it was planned. The weather hold starts only after the kept figure ends (03:00). It is right once F1 limits how old the figure can be.

Report: reports/OPT-27-review-report.md.

**OPT-27 review (Opus), head 60aa7f3 → 307cb47 (text only). Verdict: changes requested, not merged.** The fix works for the case on the card: with sensors stuck at 17:05, the 22:00 night run is planned with the cycle-cost gap. The kept figure is counted once. The OPT-28 shortfall equals the fresh-figure case. Modes off, arbitrage, hourly, fresh state-only, no forecast, unavailable forecast and N=1 replay unchanged (17 digests). Local suite: 91/0. CI on 60aa7f3 (1455/1456) and 307cb47 (1457/1458) is green. 6 of 7 mutations were caught. | # | Sev | Where | Finding | |---|---|---|---| | F1 | **Medium** | `app/solarfc.py:333-337` (`Hold.stand_in`, the evening branch) | The evening branch has no age limit. A reading taken at 00:05 on day D (sensors dead after that) comes back at 20:00 on D with `kept=True` and runs that night's plan. As today's figure, it stays in use until 03:00 on D+1, so it is up to 27 h old. Probe: 10:00/19:59 none, 20:00/23:00/02:30 kept "read 00:05", 03:01 none. This breaks the brief's "never use a figure older than one night", and DOCS.md:560 / DECISIONS:29 say "never older than that night". Behaviour fix needed: for example, branch (b) only for a reading at most ~12 h old (a 17:05 or 16:00 reading still reaches 03:00). Add a test for it. | | F2 | Low | `app/solarfc.py:333-334` | `now < day0 + timedelta(days=1)` is not the next local midnight on a DST day. On the 25 h autumn Sunday (e.g. 2027-10-31), the kept today figure is dropped from 23:00 to 24:00 local: "none (stale)", no day plan for that hour. Use `_bounds(now)[1]`. Spring is fine. One-line fix, same round as F1. | | F3 | Low (text, **fixed in 307cb47**) | `ui_text/en.json:363`, `nl.json:303` | With modes off the forecast is still read and the line shows in Settings › Simple and the Forecast tab. "used for tonight's plan" was false there (UX-53). NL "vanavond" was wrong after midnight. Now: "Solar forecast read {at}, kept for tonight." / "Zonneprognose van {at}, bewaard voor vannacht." DECISIONS "Modes off (nothing reads the sensors)" is corrected. | | F4 | Low | `app/solarfc.py:333` (BELL_TO_H = 20:00) | If a today sensor stops before 17:00 (winter, or sites further north), there is a gap from the last change + 3 h until 20:00 with no forecast (probe: stopped 16:00 → none 19:01-19:59). Card sketch. | | F5 | Low (test) | `test_opt27.py` | Mutation "never `forget_before`" survives. Lookups are by day, so it is harmless to control, but the `solar_fc_hold` store entry then grows every day. No test checks the pruning. | | F6 | Info | `app/solarfc.py:328` | A daytime outage is now covered for up to 3 h. A state-only figure is then counted from `now`, not from `at` (OPT-30 / F2 of the OPT-23-28 review). Before, an unavailable sensor gave no forecast. OPT-30 should include this case. | | F7 | Info | `test_opt27.py` part g | Part g drives the real control cycle (read_inputs + cycle, 20 min at 22:00) and compares kept with fresh exactly. At 22:00 today's state-only figure counts 0 either way, so "counted from when taken" is only checked after midnight by a solarfc-level probe (kept-as-today = fresh: 0.150 / 12.000 / 4.691 kWh). | Check 4 (TARIFF-24): a kept figure makes `solar_fc` not None, so the narrow gap applies in control and in the Forecast tab (1c9fefd) alike. That is the card's intent: the run is judged with the forecast that was fresh when it was planned. The weather hold starts only after the kept figure ends (03:00). It is right once F1 limits how old the figure can be. Report: `reports/OPT-27-review-report.md`.
admin added 1 commit 2026-10-30 20:57:44 +01:00
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
13b61d586a
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>
admin added 1 commit 2026-10-30 21:29:32 +01:00
OPT-27 round 3: the kept-forecast wording states the 12 h limit (text only)
checks / unit (pull_request) Successful in 15m27s
infra-pin / fresh (pull_request) Successful in 1s
b9dea6d90d
Co-Authored-By: Claude <noreply@anthropic.com>
admin merged commit cc9bbeb73e into release/1.0 2026-10-30 22:16:33 +01:00
Author
Owner

OPT-27 re-review (round 3, head b9dea6d): approved and merged.

  • F1: KEPT_MAX_AGE_S (12 h) is enforced in Hold.stand_in. A reading taken at 00:05 is no longer used at 20:00, 23:00 or 02:30. A normal 17:05 reading is still kept until 03:00, and a 15:01 reading until 02:59.
  • F2: the evening ends at _bounds(day0)[1], so on the 25 h day the figure is kept from 23:00 to 23:59.
  • F5: the stored hold is pruned, and a test checks it over 10 days.
  • Mutants, all red in test_opt27: no age limit (3 FAIL), day0+1 day (2 FAIL), no pruning (2 FAIL).
  • Behaviour: the app diff since 307cb47 is only these two checks, and the 3 changed digests are identical to the 60aa7f3 digests.
  • Tests: the local suite passes on b9dea6d (91 files, 0 failed). CI run 1461 checks / unit passed on b9dea6d (91 files, 0 failed).

Merged with head_commit_id b9dea6d. Merge sha cc9bbeb73e (ls-remote release/1.0).

**OPT-27 re-review (round 3, head b9dea6d): approved and merged.** - F1: `KEPT_MAX_AGE_S` (12 h) is enforced in `Hold.stand_in`. A reading taken at 00:05 is no longer used at 20:00, 23:00 or 02:30. A normal 17:05 reading is still kept until 03:00, and a 15:01 reading until 02:59. - F2: the evening ends at `_bounds(day0)[1]`, so on the 25 h day the figure is kept from 23:00 to 23:59. - F5: the stored hold is pruned, and a test checks it over 10 days. - Mutants, all red in test_opt27: no age limit (3 FAIL), day0+1 day (2 FAIL), no pruning (2 FAIL). - Behaviour: the app diff since 307cb47 is only these two checks, and the 3 changed digests are identical to the 60aa7f3 digests. - Tests: the local suite passes on b9dea6d (91 files, 0 failed). CI run 1461 `checks / unit` passed on b9dea6d (91 files, 0 failed). Merged with `head_commit_id` b9dea6d. Merge sha **cc9bbeb73e49c83a888e3cf4ca114de64e94d9d7** (ls-remote `release/1.0`).
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#219