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 master2932f3a). OPT-27 is about that code (the 1.5x widening, weather_held), so this branch starts with a clean merge of master2932f3a into release/1.0 (commit 7433786, no conflicts), then two commits of their own:
6656535tools/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=).
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
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>
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
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.
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`.
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>
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_idb9dea6d. 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`).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 inhotfix-0.19.1, thenmaster2932f3a). OPT-27 is about that code (the 1.5x widening,weather_held), so this branch starts with a clean merge ofmaster2932f3aintorelease/1.0(commit7433786, no conflicts), then two commits of their own:6656535tools/ux54_scenarios.py: with master in,test_ux54.pyfailed 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.cdcbe7dand1eb572f: 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.Holdkeeps 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_fcstays 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.pyandtest_modes_p4.pytest "the forecast is lost", so they now lose it with a hold that never covers (andtest_modes_p4.pygains 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 inreports/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): insolarfc.pytheForecastdataclass gains a last fieldkept(just abovebetween); everything else is new code (Hold,_COVERABLE) or inread_sum()/_tomorrow().dayview.pyandhomeview.py: one call each toline_solar(..., kept=).🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
f095ca9268to60aa7f310cOPT-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) and307cb47(1457/1458) is green. 6 of 7 mutations were caught.app/solarfc.py:333-337(Hold.stand_in, the evening branch)kept=Trueand 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.app/solarfc.py:333-334now < 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.307cb47)ui_text/en.json:363,nl.json:303app/solarfc.py:333(BELL_TO_H = 20:00)test_opt27.pyforget_before" survives. Lookups are by day, so it is harmless to control, but thesolar_fc_holdstore entry then grows every day. No test checks the pruning.app/solarfc.py:328now, not fromat(OPT-30 / F2 of the OPT-23-28 review). Before, an unavailable sensor gave no forecast. OPT-30 should include this case.test_opt27.pypart gCheck 4 (TARIFF-24): a kept figure makes
solar_fcnot 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 re-review (round 3, head
b9dea6d): approved and merged.KEPT_MAX_AGE_S(12 h) is enforced inHold.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._bounds(day0)[1], so on the 25 h day the figure is kept from 23:00 to 23:59.307cb47is only these two checks, and the 3 changed digests are identical to the60aa7f3digests.b9dea6d(91 files, 0 failed). CI run 1461checks / unitpassed onb9dea6d(91 files, 0 failed).Merged with
head_commit_idb9dea6d. Merge shacc9bbeb73e(ls-remoterelease/1.0).