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.pyStaticProfile.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).
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
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>
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
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.
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.
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.
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.
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.
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.02026-10-26 13:59:22 +01:00
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.
UX-53: Home shows a grid charge the add-on will not take (PRICE-02 rig test F-1 and F-2).
What changed
Controller._mode_roles()files the gate it just asked (charge_ok/trade_why) asmode_gate(None = allowed, elsepeak,not_worth,not_covered). While it is set,homeview.mode_runs()draws no grid charge andprice_windows()marks no cheap quarter. Control never reads it.homeview.leg_taken()askstrade_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).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.Changed strings
The fixed-product sentence is unchanged for a real fixed product.
static_tariff.pyStaticProfile.words(:516) andvalidate.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_worthinside 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 whendecide()acts on it, wording EN/NL, fixed vs dynamic vs day/night, day/night and hourly sweeps unchanged, price-off.test_ux53.py.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.goodwe-addon-checks:1(--network none, hwsim mounted,GOODWE_REQUIRE_HWSIM=1), includingtest_messages.py.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
UX-53 review (Opus): head
242cf43, plus one text commit5dfe972Verdict: correct. Merging once a real CI run is green on
5dfe972. No finding blocks.What I checked
mode_gateis written in_mode_roles()(main.py:2387) and read only inhomeview.py:227and:405.trade_verdict()→TariffPlan.arbitrage()andrefusal_key()have no state, logs or counters. Identity digests base5142569vs head arecmp-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_only40bd1fce…, p3_both_chcd347a04…and p3_ch_sell1cf18f9a…. The head hash is also the same when the full Home (EN+NL) is rendered every minute. So rendering Home does not change control.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.give_whychange is right: it is the same "of the periods left" case. Changing the refusal inTariffPlan.arbitrage()was the right place; static_tariff.py:516 and validate.py:1411 really are one-price profiles.give_whyback to "gepubliceerde") caught; R3 survived, see L-2.242cf43: 83/83 pass (--network none, hwsim at pin 3b4ff84,GOODWE_REQUIRE_HWSIM=1).Findings
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 ispeak, or add theno_tradeline.plan_need()not None). Mutation R3 ((gate is None or need is not None)inmode_runs) survives. The code is right: one conjunction covers both paths. Add a forecast case in a follow-up.not_worth, a sell block ("Sells up to …") is still drawn, but the add-on only gives to the house (sell_okFalse). Same kind of issue as F-1, for selling. The owner has selling off.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 (commit5dfe972, text only).meter_source: off→not_coveredall 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)