UX-77: Home draws today's grid charge for the hours control will really charge #234

Merged
admin merged 1 commits from UX-77 into release/1.0 2026-11-06 00:11:04 +01:00
Owner

UX-77 fix A (owner choice 2026-11-02): Home draws today's grid charge for the hours control will really charge, not only the run under way. Display only.

  • app/cheaproll.py: one helper rolls the cheap run the way control does (the loop of dayview._levels(), now shared): modetrade.energy() + priceruns.plan() at each run end until nothing is short or the next run does not follow on.
  • Levels at each run end are read at the charge power the peak limit leaves (peak aim less the house profile's use for that hour, shared by rating, x EFF; the rating where there is no house profile or no aim).
  • homeview.mode_runs() (grid block, Today line) and price_windows() (chart cheap bars) use it for today; each battery's block ends at its own stop. "Up to X kWh, to Y %" stays the battery's need.
  • No control change (commands, levels, runs, latch, OPT-36 netting).
  • test_ux77.py: real Controller._mode_roles() + homeview.build() stepped each minute, old and current shape, no-bind and peak-bound; red on base, green here; a mutant rolling at the full rating fails the peak-bound night; dynamic prices (no ties) draw as before.
  • test_ux54.py: the night-before scenario's today block now follows the roll (two assertions changed, see the dev report).
  • UX-54 DECISIONS paragraph and CHANGELOG line updated.

Report: reports/UX-77-dev-report.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

UX-77 fix A (owner choice 2026-11-02): Home draws today's grid charge for the hours control will really charge, not only the run under way. Display only. - `app/cheaproll.py`: one helper rolls the cheap run the way control does (the loop of `dayview._levels()`, now shared): `modetrade.energy()` + `priceruns.plan()` at each run end until nothing is short or the next run does not follow on. - Levels at each run end are read at the charge power the peak limit leaves (peak aim less the house profile's use for that hour, shared by rating, x EFF; the rating where there is no house profile or no aim). - `homeview.mode_runs()` (grid block, Today line) and `price_windows()` (chart cheap bars) use it for today; each battery's block ends at its own stop. "Up to X kWh, to Y %" stays the battery's need. - No control change (commands, levels, runs, latch, OPT-36 netting). - `test_ux77.py`: real `Controller._mode_roles()` + `homeview.build()` stepped each minute, old and current shape, no-bind and peak-bound; red on base, green here; a mutant rolling at the full rating fails the peak-bound night; dynamic prices (no ties) draw as before. - `test_ux54.py`: the night-before scenario's today block now follows the roll (two assertions changed, see the dev report). - UX-54 DECISIONS paragraph and CHANGELOG line updated. Report: `reports/UX-77-dev-report.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-11-05 22:13:24 +01:00
UX-77: Home draws today's grid charge for the hours control will really charge
checks / unit (pull_request) Successful in 27m7s
infra-pin / fresh (pull_request) Successful in 1s
45535e19d7
The cheap run is sized at the full rating, so under the peak limit a battery is
still short when it ends, and control plans the next run then. Home drew only
the run under way. `cheaproll` rolls the run on the way control does
(`modetrade.energy()` + `priceruns.plan()` at each run end, lifted from
`dayview._levels()`), reading the levels at the charge power the peak limit
leaves (peak aim less the house, else the rating). `homeview.mode_runs()` and
`price_windows()` use it for today: the grid block, the Today line and the
chart's cheap bars run to where control stops, each battery's block to its
own stop. Display only: no control change.

test_ux54: today's grid block of the night-before scenario now follows the roll.

Co-Authored-By: Claude <noreply@anthropic.com>
Author
Owner

UX-77 review (Opus): head 45535e1. Verdict: right, merging

1. Control unchanged. The diff touches only cheaproll.py, homeview.py, dayview.py, the tests, DECISIONS and CHANGELOG. main.py reaches homeview and dayview only from status_payload() and forecast_payload(), both inside try/except. for_today() only reads from the controller (_mode_packs, plan_targets, fresh_tariff_plan, peak_aim_w, weather_held, the profile's estimator(), all checked for writes).

  • Digests: I reran 6 cases, base af2d0d2 vs head. They are identical (command count, sha, net EUR, end SoC): modes off, arbitrage on (epex), current shape A+B Leftover solar (state epex, hourly real), old shape A Leftover solar + B Cheap hours (state epex, hourly real).
  • My own 24 walks also gave identical per-minute commands on base and head (day/night, a dynamic evening, a 15-minute night across midnight, N=1).

2. The roll vs control. The room is min(sum of ratings, peak aim - max(house(hour), base, 0)), shared by rating, x EFF. That is the same expression dayplan._cheap_give_kwh() uses, and what the peak guard leaves (wanted_w is measured against the aim). Without a house profile it falls back to the rating, which is sane: an upper bound, the same as before.

Probes the dev did not run, each walked minute by minute through the real _mode_roles() and homeview.build():

  • A 15-minute flat night across midnight (0.08 from 22:00 to 05:00), old and current shape, peak-bound and no bind: every charging minute is inside its battery's block. B's block ends up to 17 min after control stops.
  • N=1, A Cheap hours or Leftover solar, on a day/night night and on a 15-minute night: exact.
  • Current shape, both at 30 %, peak-bound: base misses up to 6 h of charging; head is exact.
  • A flat dynamic afternoon (0.10 from 12:00 to 17:00) with a give run at 17:00: the current shape is exact on head (base missed 150 min). The old shape over-draws on head (below).

Where Home and control still differ, both cases are old shape (Leftover solar + Cheap hours) and come from control behaviour already on OPT-25:

  • (a) Leftover solar's level climbs at each roll (OPT-25, OPT-36 rig I-2). The roll keeps today's day-plan level, so A's block ends too early. House 1.8 kW, A 40 % / B 30 %: A's level goes 56.7 → 63.4 → 70.2 → 76.8 → 83.4 → 90.1 % through the night. At 00:30 head draws A to 02:30 and control charges A to 06:00. Base drew A to 02:00, so this is no worse than base. In the test_ux54 dynamic scenario, A's level goes 50 → 56.7 → 64.1 → 71.7 %: the block ends 03:15 and control charges A to 03:45.
  • (b) Control stops with cheap hours left (OPT-25, OPT-36 rig I-1). When a run ends, control first plans without the day-plan levels. A Leftover solar battery then wants nothing, so the run is sized for B alone at its rating. That no longer fits in the cheap hours left, the cheap run moves to tomorrow, the day plan drops, and both batteries stop. The roll plans with the levels and both ratings, so the next run still fits and Home draws on. Head over-draws: the day/night 1.8 kW night by 60 min (06:00 to 07:00), and the dynamic afternoon by 150 min (14:30 to 17:00, both batteries at 33 %, cheap 0.10 before a 0.40 give). Base was exact in those two places.
  • So the display does what control would do with steady levels. When OPT-25 changes run sizing, the roll has to follow it, and OPT-25's walk tests should include an old-shape night with a moderate house (1.8 kW, A 40 / B 30).

3. test_ux54.py. Both changes follow fix A, which reverses the old "today's blocks are not extended" rule. They do not hide a new defect. The first now takes the expected block end from cheaproll.for_today() itself, so it is self-referential; test_ux77's walks are the independent check. Its A 03:15 is the case (a) gap above, not a new bug.

4. Accuracy.

  • Block end: up to +18 min. It comes from rounding up to 15 minutes plus 0.1 % SoC steps, and stays within one slot on hourly prices.
  • Tab curve at full rating vs the block (A reaches its level at 02:39, block to 03:15): OPT-25's curve item.
  • Neither blocks. For the owner's current shape every probe matched control.

5. Ties on dynamic prices. Control rolls through ties. In the 15-minute night and the flat afternoon (current shape), it charges every tied quarter to the drawn end. The roll follows it.

6. Screen text (current shape, house 2.85 kW, A 40 % / B 90 % at 22:00):

  • 22:30: A "22:00-24:00 Charges up to 8.3 kWh from the grid, to 95 %." (base: 22:00-23:00, 5.0 kWh). B "22:00-24:00 Charges up to 0.9 kWh from the grid, to 95 %." Grid blocks 22-00 on both. Cheap bars 22, 23.
  • 02:30: A "02:00-07:00 Charges up to 6.9 kWh from the grid, to 95 %." (base 02:00-04:00). B has no grid block (at 95 %). Cheap bars 02-06.
  • NL is the same with "Laadt maximaal ... van het net, tot 95 %". No string changed.
  • "up to 8.3 kWh" in a block cut at 24:00 is investigation finding S1 (the kWh is the whole night's), now more visible. Not in scope.

7. Tests.

  • My local full suite on the head (goodwe-addon-checks:1, --network none, sim mounted, GOODWE_REQUIRE_HWSIM=1, .gitea/ci/run-tests.sh): 96 files, 0 failed, 1601 s.
  • CI run 1537 (job 2688) passed on 45535e1, a real run: 96 files, 0 failed, test_ux77 and test_ux54 passed.
  • Mutants of cheaproll.py (purged copies, test_ux77.py run against each), all red:
    • room = full rating: 4 walks fail
    • no roll: 4 walks fail
    • per-battery stop ignored: 3 walks plus the unit check fail

Small notes, not blocking:

  • _mode_packs() is called without maintenance results, so a battery in maintenance counts as a charger.
  • _run_done is not modelled.
  • for_today() runs twice per build() (about 70 ms per build here; the page polls every 3 s).
  • UX-49 (a cheap bar with no charger) is still there. The owner said to fold it in on 2026-11-02, but the dev brief put it out of scope.
## UX-77 review (Opus): head 45535e1. Verdict: right, merging **1. Control unchanged.** The diff touches only `cheaproll.py`, `homeview.py`, `dayview.py`, the tests, DECISIONS and CHANGELOG. `main.py` reaches `homeview` and `dayview` only from `status_payload()` and `forecast_payload()`, both inside try/except. `for_today()` only reads from the controller (`_mode_packs`, `plan_targets`, `fresh_tariff_plan`, `peak_aim_w`, `weather_held`, the profile's `estimator()`, all checked for writes). - Digests: I reran 6 cases, base af2d0d2 vs head. They are identical (command count, sha, net EUR, end SoC): modes off, arbitrage on (epex), current shape A+B Leftover solar (state epex, hourly real), old shape A Leftover solar + B Cheap hours (state epex, hourly real). - My own 24 walks also gave identical per-minute commands on base and head (day/night, a dynamic evening, a 15-minute night across midnight, N=1). **2. The roll vs control.** The room is `min(sum of ratings, peak aim - max(house(hour), base, 0))`, shared by rating, x EFF. That is the same expression `dayplan._cheap_give_kwh()` uses, and what the peak guard leaves (`wanted_w` is measured against the aim). Without a house profile it falls back to the rating, which is sane: an upper bound, the same as before. Probes the dev did not run, each walked minute by minute through the real `_mode_roles()` and `homeview.build()`: - A 15-minute flat night across midnight (0.08 from 22:00 to 05:00), old and current shape, peak-bound and no bind: every charging minute is inside its battery's block. B's block ends up to 17 min after control stops. - N=1, A Cheap hours or Leftover solar, on a day/night night and on a 15-minute night: exact. - Current shape, both at 30 %, peak-bound: base misses up to 6 h of charging; head is exact. - A flat dynamic afternoon (0.10 from 12:00 to 17:00) with a give run at 17:00: the current shape is exact on head (base missed 150 min). The old shape over-draws on head (below). Where Home and control still differ, both cases are old shape (Leftover solar + Cheap hours) and come from control behaviour already on OPT-25: - **(a) Leftover solar's level climbs at each roll** (OPT-25, OPT-36 rig I-2). The roll keeps today's day-plan level, so A's block ends too early. House 1.8 kW, A 40 % / B 30 %: A's level goes 56.7 → 63.4 → 70.2 → 76.8 → 83.4 → 90.1 % through the night. At 00:30 head draws A to 02:30 and control charges A to 06:00. Base drew A to 02:00, so this is no worse than base. In the `test_ux54` dynamic scenario, A's level goes 50 → 56.7 → 64.1 → 71.7 %: the block ends 03:15 and control charges A to 03:45. - **(b) Control stops with cheap hours left** (OPT-25, OPT-36 rig I-1). When a run ends, control first plans without the day-plan levels. A Leftover solar battery then wants nothing, so the run is sized for B alone at its rating. That no longer fits in the cheap hours left, the cheap run moves to tomorrow, the day plan drops, and both batteries stop. The roll plans with the levels and both ratings, so the next run still fits and Home draws on. Head over-draws: the day/night 1.8 kW night by 60 min (06:00 to 07:00), and the dynamic afternoon by 150 min (14:30 to 17:00, both batteries at 33 %, cheap 0.10 before a 0.40 give). Base was exact in those two places. - So the display does what control would do with steady levels. When OPT-25 changes run sizing, the roll has to follow it, and OPT-25's walk tests should include an old-shape night with a moderate house (1.8 kW, A 40 / B 30). **3. `test_ux54.py`.** Both changes follow fix A, which reverses the old "today's blocks are not extended" rule. They do not hide a new defect. The first now takes the expected block end from `cheaproll.for_today()` itself, so it is self-referential; `test_ux77`'s walks are the independent check. Its A 03:15 is the case (a) gap above, not a new bug. **4. Accuracy.** - Block end: up to +18 min. It comes from rounding up to 15 minutes plus 0.1 % SoC steps, and stays within one slot on hourly prices. - Tab curve at full rating vs the block (A reaches its level at 02:39, block to 03:15): OPT-25's curve item. - Neither blocks. For the owner's current shape every probe matched control. **5. Ties on dynamic prices.** Control rolls through ties. In the 15-minute night and the flat afternoon (current shape), it charges every tied quarter to the drawn end. The roll follows it. **6. Screen text** (current shape, house 2.85 kW, A 40 % / B 90 % at 22:00): - 22:30: A "22:00-24:00 Charges up to 8.3 kWh from the grid, to 95 %." (base: 22:00-23:00, 5.0 kWh). B "22:00-24:00 Charges up to 0.9 kWh from the grid, to 95 %." Grid blocks 22-00 on both. Cheap bars 22, 23. - 02:30: A "02:00-07:00 Charges up to 6.9 kWh from the grid, to 95 %." (base 02:00-04:00). B has no grid block (at 95 %). Cheap bars 02-06. - NL is the same with "Laadt maximaal ... van het net, tot 95 %". No string changed. - "up to 8.3 kWh" in a block cut at 24:00 is investigation finding S1 (the kWh is the whole night's), now more visible. Not in scope. **7. Tests.** - My local full suite on the head (`goodwe-addon-checks:1`, `--network none`, sim mounted, `GOODWE_REQUIRE_HWSIM=1`, `.gitea/ci/run-tests.sh`): 96 files, 0 failed, 1601 s. - CI run 1537 (job 2688) passed on 45535e1, a real run: 96 files, 0 failed, `test_ux77` and `test_ux54` passed. - Mutants of `cheaproll.py` (purged copies, `test_ux77.py` run against each), all red: - room = full rating: 4 walks fail - no roll: 4 walks fail - per-battery stop ignored: 3 walks plus the unit check fail **Small notes, not blocking:** - `_mode_packs()` is called without maintenance results, so a battery in maintenance counts as a charger. - `_run_done` is not modelled. - `for_today()` runs twice per `build()` (about 70 ms per build here; the page polls every 3 s). - UX-49 (a cheap bar with no charger) is still there. The owner said to fold it in on 2026-11-02, but the dev brief put it out of scope.
admin merged commit 47c89a13d0 into release/1.0 2026-11-06 00:11:04 +01:00
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#234