UX-53: Home draws a grid charge only where the add-on will take it #209

Merged
admin merged 2 commits from UX-53 into release/1.0 2026-10-26 13:59:22 +01:00
Owner

UX-53: Home shows a grid charge the add-on will not take (PRICE-02 rig test F-1 and F-2).

What changed

  • F-1, modes on. Controller._mode_roles() files the gate it just asked (charge_ok / trade_why) as mode_gate (None = allowed, else peak, not_worth, not_covered). While it is set, homeview.mode_runs() draws no grid charge and price_windows() marks no cheap quarter. Control never reads it.
  • F-2, modes off. homeview.leg_taken() asks trade_verdict() (the control cycle's own call) at a verdict leg's own start. A charge after the last dear period meets a flat remainder there, is refused, and is not drawn, marked or announced ("Tomorrow: ..." too).
  • Wording. grid_why / give_why: "Lowest/Highest published price" -> "Lowest/Highest price left" (NL: "... die nog over is"). The flat-tariff refusal says "a fully fixed product" only when the product is fixed.
  • No change to control decisions, commands or options. No CHANGELOG or version change.

Changed strings

Where Old New
en.json run.grid_why Lowest published price, {eur} per kWh. Lowest price left, {eur} per kWh.
nl.json run.grid_why Laagste gepubliceerde prijs, {eur} per kWh. Laagste prijs die nog over is, {eur} per kWh.
en.json run.give_why Highest published price, {eur} per kWh. Highest price left, {eur} per kWh.
nl.json run.give_why Hoogste gepubliceerde prijs, {eur} per kWh. Hoogste prijs die nog over is, {eur} per kWh.
static_tariff.py flat refusal (Arbitrage row, English only), non-fixed product ...is arithmetically impossible. §9.2: a fully fixed product is self-consumption only ...is arithmetically impossible

The fixed-product sentence is unchanged for a real fixed product. static_tariff.py StaticProfile.words (:516) and validate.py :1411 describe a profile with one price, which is a real fixed product, and are unchanged.

Evidence

  • test_ux53.py (new): F-1 with the real _mode_roles() (gate open charges and is drawn; not_worth inside the run: no charge, no cheap bar, role hold; peak and not_covered; headroom is not the gate), F-2 (flat remainder vs allowed leg, sale kept, tomorrow line), 40 random 15-minute days where a leg is drawn exactly when decide() acts on it, wording EN/NL, fixed vs dynamic vs day/night, day/night and hourly sweeps unchanged, price-off.
  • 13 mutations, all caught by test_ux53.py.
  • Commands identical to base 5142569: tools/opt11p3_identity, opt11p4_identity, safety79_identity, safety84_identity digests equal; real-data replay (captured 15-minute prices 2026-09-09/10, 34 560 ticks each) hashed per tick for arb_only (modes off), p3_both_ch, p3_ch_sell, p3_ls (modes on): all four byte-identical.
  • Full suite 83/83 in goodwe-addon-checks:1 (--network none, hwsim mounted, GOODWE_REQUIRE_HWSIM=1), including test_messages.py.
  • Playwright, Home EN and NL, before/after, F-1 and F-2 (not committed).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

UX-53: Home shows a grid charge the add-on will not take (PRICE-02 rig test F-1 and F-2). ## What changed - **F-1, modes on.** `Controller._mode_roles()` files the gate it just asked (`charge_ok` / `trade_why`) as `mode_gate` (None = allowed, else `peak`, `not_worth`, `not_covered`). While it is set, `homeview.mode_runs()` draws no grid charge and `price_windows()` marks no cheap quarter. Control never reads it. - **F-2, modes off.** `homeview.leg_taken()` asks `trade_verdict()` (the control cycle's own call) at a verdict leg's own start. A charge after the last dear period meets a flat remainder there, is refused, and is not drawn, marked or announced ("Tomorrow: ..." too). - **Wording.** `grid_why` / `give_why`: "Lowest/Highest published price" -> "Lowest/Highest price left" (NL: "... die nog over is"). The flat-tariff refusal says "a fully fixed product" only when the product is fixed. - **No change** to control decisions, commands or options. No CHANGELOG or version change. ## Changed strings | Where | Old | New | |---|---|---| | en.json run.grid_why | Lowest published price, {eur} per kWh. | Lowest price left, {eur} per kWh. | | nl.json run.grid_why | Laagste gepubliceerde prijs, {eur} per kWh. | Laagste prijs die nog over is, {eur} per kWh. | | en.json run.give_why | Highest published price, {eur} per kWh. | Highest price left, {eur} per kWh. | | nl.json run.give_why | Hoogste gepubliceerde prijs, {eur} per kWh. | Hoogste prijs die nog over is, {eur} per kWh. | | static_tariff.py flat refusal (Arbitrage row, English only), non-fixed product | ...is arithmetically impossible. §9.2: a fully fixed product is self-consumption only | ...is arithmetically impossible | The fixed-product sentence is unchanged for a real fixed product. `static_tariff.py` `StaticProfile.words` (:516) and `validate.py` :1411 describe a profile with one price, which is a real fixed product, and are unchanged. ## Evidence - `test_ux53.py` (new): F-1 with the real `_mode_roles()` (gate open charges and is drawn; `not_worth` inside the run: no charge, no cheap bar, role hold; peak and not_covered; headroom is not the gate), F-2 (flat remainder vs allowed leg, sale kept, tomorrow line), 40 random 15-minute days where a leg is drawn exactly when `decide()` acts on it, wording EN/NL, fixed vs dynamic vs day/night, day/night and hourly sweeps unchanged, price-off. - 13 mutations, all caught by `test_ux53.py`. - Commands identical to base 5142569: tools/opt11p3_identity, opt11p4_identity, safety79_identity, safety84_identity digests equal; real-data replay (captured 15-minute prices 2026-09-09/10, 34 560 ticks each) hashed per tick for arb_only (modes off), p3_both_ch, p3_ch_sell, p3_ls (modes on): all four byte-identical. - Full suite 83/83 in `goodwe-addon-checks:1` (`--network none`, hwsim mounted, `GOODWE_REQUIRE_HWSIM=1`), including `test_messages.py`. - Playwright, Home EN and NL, before/after, F-1 and F-2 (not committed). 🤖 Generated with [Claude Code](https://claude.ai/code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-10-26 11:15:44 +01:00
UX-53: Home draws a grid charge only where the add-on will take it
checks / unit (pull_request) Successful in 48m7s
infra-pin / fresh (pull_request) Successful in 2s
242cf434f4
Modes on: Controller._mode_roles() files the trade gate it just asked as
mode_gate; while it refuses, Home draws no grid charge and marks no cheap
quarter. Modes off: a charge after the last dear period is asked of the same
gate at its own start (homeview.leg_taken) and dropped when refused there.
Words: "Lowest/Highest price left" (EN, NL); a flat remainder on a dynamic or
day/night site is no longer called a fully fixed product.

No change to control decisions, commands or options.

Co-Authored-By: Claude <noreply@anthropic.com>
admin added 1 commit 2026-10-26 13:10:51 +01:00
UX-53 review: DECISIONS text, day/night weekend case and peak guard limit
checks / unit (pull_request) Successful in 48m2s
infra-pin / fresh (pull_request) Successful in 1s
5dfe9720c5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

UX-53 review (Opus): head 242cf43, plus one text commit 5dfe972

Verdict: correct. Merging once a real CI run is green on 5dfe972. No finding blocks.

What I checked

  1. No control change. mode_gate is written in _mode_roles() (main.py:2387) and read only in homeview.py:227 and :405. trade_verdict() → TariffPlan.arbitrage() and refusal_key() have no state, logs or counters. Identity digests base 5142569 vs head are cmp-equal: opt11p3 (6), opt11p4 (12), safety84 (17). Real-data replay, 34 560 ticks per run, every tick hashed (commands, grid, arbitrage action/watts/sentence, winning claim, roles, mode tick action/why/ask). Base and head are equal for arb_only 40bd1fce…, p3_both_ch cd347a04… and p3_ch_sell 1cf18f9a…. The head hash is also the same when the full Home (EN+NL) is rendered every minute. So rendering Home does not change control.
  2. Home follows the decision. F-1 and F-2 are fixed. Modes-off legs: 5 other seeds × 80 random days, 0 disagreements with decide(). Replay, Home every 5 min vs the add-on's actual charging: modes off, "drawn but not taken" 630 → 0 probe-minutes. Modes on: one probe differs from base. See L-1.
  3. Text. EN and NL are correct and short. "die nog over is" is plain Dutch with the same meaning. The give_why change is right: it is the same "of the periods left" case. Changing the refusal in TariffPlan.arbitrage() was the right place; static_tariff.py:516 and validate.py:1411 really are one-price profiles.
  4. Tests. I re-ran M1, M3, M4, M5, M6 and M11: all caught. Mine: R1 (sale leg never filtered) caught; R2 (NL give_why back to "gepubliceerde") caught; R3 survived, see L-2.
  5. Browser. Playwright, Home EN and NL, base and head, for F-1, F-2, day/night and price off: 16 loads with no page or console errors. Head draws no refused charge. Day/night and price off are the same as base apart from the words.
  6. Offline suite on 242cf43: 83/83 pass (--network none, hwsim at pin 3b4ff84, GOODWE_REQUIRE_HWSIM=1).

Findings

  • L-1 (low, UX). homeview.py:405, :422-425, :227. With modes on, while the peak guard holds (mode_gate == "peak"), Home hides the whole rest of the cheap run. The add-on charges in it again once the guard clears. In the replay: one 5-minute probe (13:34 on 2026-09-09) hid 46/91 minutes the add-on charged later. It lasts as long as the guard holds. The modes-off page already does the same, and no reason line is shown. Suggestion for a follow-up: draw the run while the gate is peak, or add the no_trade line.
  • L-2 (low, test gap). test_ux53.py. No check covers the P4 day-plan path (plan_need() not None). Mutation R3 ((gate is None or need is not None) in mode_runs) survives. The code is right: one conjunction covers both paths. Add a forecast case in a follow-up.
  • L-3 (low, out of scope). homeview.py:426-429. With modes on and not_worth, a sell block ("Sells up to …") is still drawn, but the add-on only gives to the house (sell_ok False). Same kind of issue as F-1, for selling. The owner has selling off.
  • I-1 (info). F-2 does reach day/night sites: on a Belgian profile with an all-night weekend, base drew a Friday 22:00 charge that decide() refuses ("the tariff is flat"), and head no longer draws it. The dev report says day/night is unchanged. I corrected the DECISIONS note (commit 5dfe972, text only).
  • I-2 (info). Modes on with meter_source: off → not_covered all the time. Home now never draws a grid charge, which is right, but it gives no reason line.

My text change (5dfe972, DECISIONS/UX-53-home-follows-the-gate.md only)

  • "What does not change": adds the day/night exception (all-night weekend, Friday 22:00).
  • "Limits": the peak-guard sentence now says the whole rest of the run is hidden.
  • Re-wrapped one line that ran over 80 characters.
## UX-53 review (Opus): head 242cf43, plus one text commit 5dfe972 **Verdict: correct. Merging once a real CI run is green on 5dfe972.** No finding blocks. ### What I checked 1. **No control change.** `mode_gate` is written in `_mode_roles()` (main.py:2387) and read only in `homeview.py:227` and `:405`. `trade_verdict()` → `TariffPlan.arbitrage()` and `refusal_key()` have no state, logs or counters. Identity digests base 5142569 vs head are `cmp`-equal: opt11p3 (6), opt11p4 (12), safety84 (17). Real-data replay, 34 560 ticks per run, every tick hashed (commands, grid, arbitrage action/watts/sentence, winning claim, roles, mode tick action/why/ask). Base and head are equal for arb_only `40bd1fce…`, p3_both_ch `cd347a04…` and p3_ch_sell `1cf18f9a…`. The head hash is also the same when the full Home (EN+NL) is rendered every minute. So rendering Home does not change control. 2. **Home follows the decision.** F-1 and F-2 are fixed. Modes-off legs: 5 other seeds × 80 random days, 0 disagreements with `decide()`. Replay, Home every 5 min vs the add-on's actual charging: modes off, "drawn but not taken" 630 → 0 probe-minutes. Modes on: one probe differs from base. See L-1. 3. **Text.** EN and NL are correct and short. "die nog over is" is plain Dutch with the same meaning. The `give_why` change is right: it is the same "of the periods left" case. Changing the refusal in `TariffPlan.arbitrage()` was the right place; static_tariff.py:516 and validate.py:1411 really are one-price profiles. 4. **Tests.** I re-ran M1, M3, M4, M5, M6 and M11: all caught. Mine: R1 (sale leg never filtered) caught; R2 (NL `give_why` back to "gepubliceerde") caught; R3 survived, see L-2. 5. **Browser.** Playwright, Home EN and NL, base and head, for F-1, F-2, day/night and price off: 16 loads with no page or console errors. Head draws no refused charge. Day/night and price off are the same as base apart from the words. 6. **Offline suite** on 242cf43: 83/83 pass (`--network none`, hwsim at pin 3b4ff84, `GOODWE_REQUIRE_HWSIM=1`). ### Findings - **L-1 (low, UX). homeview.py:405, :422-425, :227.** With modes on, while the peak guard holds (`mode_gate == "peak"`), Home hides the whole rest of the cheap run. The add-on charges in it again once the guard clears. In the replay: one 5-minute probe (13:34 on 2026-09-09) hid 46/91 minutes the add-on charged later. It lasts as long as the guard holds. The modes-off page already does the same, and no reason line is shown. Suggestion for a follow-up: draw the run while the gate is `peak`, or add the `no_trade` line. - **L-2 (low, test gap). test_ux53.py.** No check covers the P4 day-plan path (`plan_need()` not None). Mutation R3 (`(gate is None or need is not None)` in `mode_runs`) survives. The code is right: one conjunction covers both paths. Add a forecast case in a follow-up. - **L-3 (low, out of scope). homeview.py:426-429.** With modes on and `not_worth`, a sell block ("Sells up to …") is still drawn, but the add-on only gives to the house (`sell_ok` False). Same kind of issue as F-1, for selling. The owner has selling off. - **I-1 (info).** F-2 does reach day/night sites: on a Belgian profile with an all-night weekend, base drew a Friday 22:00 charge that `decide()` refuses ("the tariff is flat"), and head no longer draws it. The dev report says day/night is unchanged. I corrected the DECISIONS note (commit 5dfe972, text only). - **I-2 (info).** Modes on with `meter_source: off` → `not_covered` all the time. Home now never draws a grid charge, which is right, but it gives no reason line. ### My text change (5dfe972, DECISIONS/UX-53-home-follows-the-gate.md only) - "What does not change": adds the day/night exception (all-night weekend, Friday 22:00). - "Limits": the peak-guard sentence now says the whole rest of the run is hidden. - Re-wrapped one line that ran over 80 characters.
admin merged commit a1d07030f8 into release/1.0 2026-10-26 13:59:22 +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#209