UX-13 P1: page shell and Home screen on a structured /status #144

Merged
admin merged 2 commits from UX-13-p1-home into release/1.0 2026-09-28 01:29:31 +02:00
Owner

UX-13 phase P1 (card 6ab979852f3eca02fb40f69e): the new page shell and the Home screen, built on a structured /status. Design: goodwe-session/UX-13-design.md §1, §3.3, §4, and the approved mockup v2.

What changed

  • Page shell (app/static/index.html, app.css, app.js). It has a header with a status pill and four tabs (Home, Batteries, Settings, Setup) with hash routes. Under 640 px the tabs move to the bottom. Colours follow the mockup and prefers-color-scheme. The code is plain JS with no framework, no CDN and no web fonts. web.py joins the three files and ui_text/en.json into one response at /. The route list is unchanged (test_selfconfig part 7 is untouched), and every URL is relative.
  • Structured /status (app/homeview.py, read-only). It adds state, now, batteries[], plan, attention[], month_peak and control beside the old banner, level, rows, checks, hint and events, which are unchanged. If the Home view raises, the old fields are still served with home_error: true, the page falls back to the installer details, and one ERROR line is logged per process.
  • Home:
    • Banner: the existing cascade plus a short sub-line.
    • Now: the tiles are grid, solar (pv_w) and battery (site_batt_w); house = grid + solar + battery, and it is shown only when solar is measured. Each battery has a level bar and a "now" line.
    • Today's plan: price bars come from the fresh tariff plan (or the static profile), with cheap/expensive marked by thirds of today's range. There is one lane per battery.
    • Peak limit: from PeakTracker and the guard's projection.
    • Needs attention: every non-green check row, with a plain title per row id and the row's own sentence as the body.
    • Control: start and stop, each with a confirm dialog. The page reads live state first, because auto_toggle is a toggle.
  • Only plans that exist today (no OPT-11 modes):
    • Trading on price uses the same plan.arbitrage() call and inputs as cycle(). It draws a charge block at the cheapest point and a give block at the dearest, on every battery. kWh is capped by the battery's rating and by the room its level leaves inside arbitrage's floor/ceiling; € = kWh × that price. A refused verdict (peak guard holding, guard not covering, stale plan, gap too small) draws nothing and gives a plain reason.
    • The monthly cycle: one battery at a time, as maintenance.arbitrate does.
    • The peak limit is drawn as a block only while it is engaged.
    • Otherwise each battery "follows the house".
    • Shadow mode uses "would …" wording everywhere. On a red banner (every inverter at 0 W) nothing is planned.
  • Tooltips work on hover, on tap and on keyboard focus. A price bar shows the hour and the price (plus cheap/expensive/estimate). A block shows what, when, why, kWh and €. Chart items use a roving tabindex with arrow keys and have text aria-labels. The plan is also listed in words.
  • Region preset year (P0 follow-up):
    • config/model returns region: {id, year} for P2's Settings screens.
    • Home's price line says "Levies and VAT: Flanders, Fluvius Antwerpen, 2026 figures." (or "your own figure") when prices come from a feed.
    • The Settings tab shows the same line.
    • Note for P2: render the year beside the region choice.
  • Accessibility and no colour alone: the tabs are a keyboard tablist (arrows, Home/End). Expensive hours and give/cycle blocks are patterned. Severity is shown by icon + screen-reader word + colour. Dialogs use <dialog>. Reduced motion is respected.
  • Placeholders: Batteries shows each battery (level, stored, now, mode) and links to the settings page. Settings and Setup link to the existing config page until P2/P4.

Regression check: everything the old page did is still reachable

The old functions are all still there:

  • Start/stop is the Control card, now with a confirm dialog.
  • "Force maintenance cycle" and "Abort maintenance" are under Control → More actions, both with a confirm dialog.
  • The status rows and the commissioning checks are in "Details for installers". It opens by itself if the structured view is missing.
  • The event line (hint) is under the Control buttons.
  • The link to the configuration page is on the Settings, Setup and Batteries tabs.
  • events stays in the JSON.

test_home part 5 pins all of these.

Tests

  • test_home.py (new):
    • pins the old and new /status fields;
    • checks that trade blocks sit where the cycle's verdict puts them, and pins their kWh/€;
    • checks that a past, peak-held, uncovered, flat or stale plan draws no trade;
    • covers maintenance (due, under way, one at a time), shadow, stopped, alarm, and fault isolation;
    • checks the page: assembled, no external URL, relative URLs, every label in ui_text, no innerHTML, no duplicate ids;
    • checks GET /, GET status and config/model region over HTTP.
  • test_messages.py part 8: the STYLE scan of all 150+ rendered Home sentences (titles ≤5 words, plan lines ≤2 sentences × 12 words, other lines 1 sentence ≤25 words), a "would" check for shadow variants, and a mutation. The new ui_text "home" labels go through the existing part 3 scan. STYLE.md gains the "plan line" row type.
  • test_pv.py: homeview.py is added to the files allowed to read the solar figure. It is a read-only view (the House tile), and the decider exclusion list is unchanged.
  • peak_cap_w is read from controller.peak.cap_w, so test_defaults_pin's ledger does not grow.
  • All 41 suites pass in the CI image (goodwe-addon-checks:1, read-only mount, GOODWE_REQUIRE_HWSIM=1, with the sibling goodwe repo mounted for test_freeze).

Headless render check (Chromium 152 in a container, via CDP)

Setup: tools/ux13_demo.py STATE PORT serves the real web.build_app over a fixture Controller (two batteries, solar, today's prices in the mockup's shape, trading on). I drove it in the running, shadow and alarm states, in light and dark, at 1000 px and 390 px.

Results:

  • No JS exception and no console error. The only error entry is the browser's own favicon.ico 404, which does not apply under ingress.
  • No horizontal scroll at 390 px, and the tabs are fixed at the bottom.
  • Hover on the 19:00 bar: "19:00 to 20:00 · €0.36 per kWh, an expensive hour."
  • Keyboard focus, then ArrowRight to the next block: "Battery A, 13:00 to 14:00 · Charges up to 2.5 kWh from the grid. · Cheapest price in the list, €0.14 per kWh. · Costs up to €0.35."
  • A touch tap on a bar shows its tooltip.
  • ArrowRight on the Home tab selects Batteries and sets #batteries.
  • Stop opens "Stop control? Every inverter is set to 0 W until you start control again."
  • Shadow: every block and the head say "would", the battery tile says "Held at 0 W", and "Shadow mode is on" is the first item under Needs attention.
  • Alarm: the pill says "Needs you now", the red item is first, and the plan says "Held at 0 W" with no blocks.

Screenshots are attached below.

Not in this PR / notes

  • TEST-23 live check is pending. The stack is in use by SAFETY-73 (dev-S73-* clones exist), so I did not touch it. That check also covers P0's GET ha/entities.
  • Finding, not changed here: maintenance.wants_start() compares maintenance_start_hour with the UTC hour (datetime.now(timezone.utc).hour), while DOCS.md says "the local hour". So 10 means 12:00 in Belgian summer. The Home plan shows the time the code really uses, in local time. This needs its own card.
  • /config still serves the UX-02 page until P2 replaces it.

Screenshots (headless Chromium, fixture data)

running-desktop-light

running-desktop-light

running-phone-dark

running-phone-dark

running-tip-bar

running-tip-bar

running-tip-block

running-tip-block

shadow-desktop-light

shadow-desktop-light

alarm-desktop-light

alarm-desktop-light

running-dialog

running-dialog

running-batteries

running-batteries

running-settings

running-settings

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p

UX-13 phase P1 (card 6ab979852f3eca02fb40f69e): the new page shell and the Home screen, built on a structured `/status`. Design: `goodwe-session/UX-13-design.md` §1, §3.3, §4, and the approved mockup v2. ## What changed - **Page shell** (`app/static/index.html`, `app.css`, `app.js`). It has a header with a status pill and four tabs (Home, Batteries, Settings, Setup) with hash routes. Under 640 px the tabs move to the bottom. Colours follow the mockup and `prefers-color-scheme`. The code is plain JS with no framework, no CDN and no web fonts. `web.py` joins the three files and `ui_text/en.json` into one response at `/`. **The route list is unchanged** (test_selfconfig part 7 is untouched), and every URL is relative. - **Structured `/status`** (`app/homeview.py`, read-only). It adds `state`, `now`, `batteries[]`, `plan`, `attention[]`, `month_peak` and `control` beside the old `banner`, `level`, `rows`, `checks`, `hint` and `events`, which are unchanged. If the Home view raises, the old fields are still served with `home_error: true`, the page falls back to the installer details, and one ERROR line is logged per process. - **Home**: - **Banner**: the existing cascade plus a short sub-line. - **Now**: the tiles are grid, solar (`pv_w`) and battery (`site_batt_w`); house = grid + solar + battery, and it is shown only when solar is measured. Each battery has a level bar and a "now" line. - **Today's plan**: price bars come from the fresh tariff plan (or the static profile), with cheap/expensive marked by thirds of today's range. There is one lane per battery. - **Peak limit**: from `PeakTracker` and the guard's projection. - **Needs attention**: every non-green check row, with a plain title per row id and the row's own sentence as the body. - **Control**: start and stop, each with a confirm dialog. The page reads live state first, because `auto_toggle` is a toggle. - **Only plans that exist today** (no OPT-11 modes): - Trading on price uses the **same `plan.arbitrage()` call and inputs as `cycle()`**. It draws a charge block at the cheapest point and a give block at the dearest, on every battery. kWh is capped by the battery's rating and by the room its level leaves inside arbitrage's floor/ceiling; € = kWh × that price. A refused verdict (peak guard holding, guard not covering, stale plan, gap too small) draws nothing and gives a plain reason. - The monthly cycle: one battery at a time, as `maintenance.arbitrate` does. - The peak limit is drawn as a block only while it is engaged. - Otherwise each battery "follows the house". - Shadow mode uses "would …" wording everywhere. On a red banner (every inverter at 0 W) nothing is planned. - **Tooltips** work on hover, on tap and on keyboard focus. A price bar shows the hour and the price (plus cheap/expensive/estimate). A block shows what, when, why, kWh and €. Chart items use a roving tabindex with arrow keys and have text aria-labels. The plan is also listed in words. - **Region preset year** (P0 follow-up): - `config/model` returns `region: {id, year}` for P2's Settings screens. - Home's price line says "Levies and VAT: Flanders, Fluvius Antwerpen, 2026 figures." (or "your own figure") when prices come from a feed. - The Settings tab shows the same line. - **Note for P2**: render the year beside the region choice. - **Accessibility and no colour alone**: the tabs are a keyboard tablist (arrows, Home/End). Expensive hours and give/cycle blocks are patterned. Severity is shown by icon + screen-reader word + colour. Dialogs use `<dialog>`. Reduced motion is respected. - **Placeholders**: Batteries shows each battery (level, stored, now, mode) and links to the settings page. Settings and Setup link to the existing `config` page until P2/P4. ## Regression check: everything the old page did is still reachable The old functions are all still there: - Start/stop is the Control card, now with a confirm dialog. - "Force maintenance cycle" and "Abort maintenance" are under Control → More actions, both with a confirm dialog. - The status rows and the commissioning checks are in "Details for installers". It opens by itself if the structured view is missing. - The event line (`hint`) is under the Control buttons. - The link to the configuration page is on the Settings, Setup and Batteries tabs. - `events` stays in the JSON. test_home part 5 pins all of these. ## Tests - `test_home.py` (new): - pins the old and new `/status` fields; - checks that trade blocks sit where the cycle's verdict puts them, and pins their kWh/€; - checks that a past, peak-held, uncovered, flat or stale plan draws no trade; - covers maintenance (due, under way, one at a time), shadow, stopped, alarm, and fault isolation; - checks the page: assembled, no external URL, relative URLs, every label in ui_text, no `innerHTML`, no duplicate ids; - checks `GET /`, `GET status` and `config/model` region over HTTP. - `test_messages.py` part 8: the STYLE scan of all 150+ rendered Home sentences (titles ≤5 words, plan lines ≤2 sentences × 12 words, other lines 1 sentence ≤25 words), a "would" check for shadow variants, and a mutation. The new `ui_text` "home" labels go through the existing part 3 scan. STYLE.md gains the "plan line" row type. - `test_pv.py`: `homeview.py` is added to the files allowed to read the solar figure. It is a read-only view (the House tile), and the decider exclusion list is unchanged. - `peak_cap_w` is read from `controller.peak.cap_w`, so test_defaults_pin's ledger does not grow. - All 41 suites pass in the CI image (`goodwe-addon-checks:1`, read-only mount, `GOODWE_REQUIRE_HWSIM=1`, with the sibling `goodwe` repo mounted for test_freeze). ## Headless render check (Chromium 152 in a container, via CDP) Setup: `tools/ux13_demo.py STATE PORT` serves the real `web.build_app` over a fixture `Controller` (two batteries, solar, today's prices in the mockup's shape, trading on). I drove it in the running, shadow and alarm states, in light and dark, at 1000 px and 390 px. Results: - **No JS exception and no console error.** The only error entry is the browser's own `favicon.ico` 404, which does not apply under ingress. - No horizontal scroll at 390 px, and the tabs are fixed at the bottom. - Hover on the 19:00 bar: "19:00 to 20:00 · €0.36 per kWh, an expensive hour." - Keyboard focus, then ArrowRight to the next block: "Battery A, 13:00 to 14:00 · Charges up to 2.5 kWh from the grid. · Cheapest price in the list, €0.14 per kWh. · Costs up to €0.35." - A touch tap on a bar shows its tooltip. - ArrowRight on the Home tab selects Batteries and sets `#batteries`. - Stop opens "Stop control? Every inverter is set to 0 W until you start control again." - Shadow: every block and the head say "would", the battery tile says "Held at 0 W", and "Shadow mode is on" is the first item under Needs attention. - Alarm: the pill says "Needs you now", the red item is first, and the plan says "Held at 0 W" with no blocks. Screenshots are attached below. ## Not in this PR / notes - **TEST-23 live check is pending.** The stack is in use by SAFETY-73 (`dev-S73-*` clones exist), so I did not touch it. That check also covers P0's `GET ha/entities`. - **Finding, not changed here:** `maintenance.wants_start()` compares `maintenance_start_hour` with the **UTC** hour (`datetime.now(timezone.utc).hour`), while DOCS.md says "the local hour". So 10 means 12:00 in Belgian summer. The Home plan shows the time the code really uses, in local time. This needs its own card. - `/config` still serves the UX-02 page until P2 replaces it. ## Screenshots (headless Chromium, fixture data) **running-desktop-light** ![running-desktop-light](https://gittea.kammenstraatha.duckdns.org/attachments/ea5e4205-f76f-4e49-b3ef-339488a40b95) **running-phone-dark** ![running-phone-dark](https://gittea.kammenstraatha.duckdns.org/attachments/2da0d8eb-185e-4686-bf6f-b13dd6ec401b) **running-tip-bar** ![running-tip-bar](https://gittea.kammenstraatha.duckdns.org/attachments/a197df3d-33fa-487c-aa8e-5a30833da1cf) **running-tip-block** ![running-tip-block](https://gittea.kammenstraatha.duckdns.org/attachments/9f1bab72-3759-4788-b6ca-38b1536bff64) **shadow-desktop-light** ![shadow-desktop-light](https://gittea.kammenstraatha.duckdns.org/attachments/e30270a4-c98a-42af-aeac-db96f98c7bc9) **alarm-desktop-light** ![alarm-desktop-light](https://gittea.kammenstraatha.duckdns.org/attachments/2c9057af-bb75-469f-b104-07a54373120c) **running-dialog** ![running-dialog](https://gittea.kammenstraatha.duckdns.org/attachments/50d39793-8f42-4833-8cd8-1956ed0abf33) **running-batteries** ![running-batteries](https://gittea.kammenstraatha.duckdns.org/attachments/b464b119-1931-4c93-9946-2b16900b906d) **running-settings** ![running-settings](https://gittea.kammenstraatha.duckdns.org/attachments/4baf2ff0-7271-40d7-9057-b508e3277df1) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
admin added 1 commit 2026-09-28 00:45:37 +02:00
UX-13 P1: page shell and Home screen on a structured /status
checks / unit (pull_request) Successful in 9m37s
infra-pin / fresh (pull_request) Successful in 2s
f8d63cc614
- app/static/{index.html,app.css,app.js}: tabs (Home, Batteries, Settings,
  Setup), hash routes, phone bottom tabs, light/dark. Served inlined at "/";
  the route list is unchanged, and every URL is relative.
- app/homeview.py: /status gains state, now, batteries, plan, attention,
  month_peak and control beside the old fields. The plan is what the add-on
  does today: the arbitrage verdict (the same plan.arbitrage() call as the
  cycle), the monthly cycle, the peak limit, "follows the house".
- The Home plan chart has hover, tap and keyboard-focus tooltips: price bar =
  hour and price; block = what, when, why, kWh and EUR.
- Region preset and year: in config/model (for P2) and on the Home price line.
- test_home.py; test_messages part 8 scans every Home sentence; STYLE plan line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
Author
Owner

Review of UX-13 P1 (head f8d63cc)

Verified, no finding

  • plan.arbitrage() gets the cycle's own inputs: arbitrage.guard_state(c.peak_guard), threshold_eur_kwh(c.o), {NO_WEATHER}, on c.fresh_tariff_plan(now). Home trades only on the fresh plan, as the cycle does. The static profile only colours the bars.
  • Read-only: after status_payload() the controller has no new or rebound attributes. Only the display cache home_prices fills in place. The plan's points are unchanged.
  • Cost: status_payload() takes 2.8 ms (two batteries, 48 points) in the CI image. That is fine at a 3 s poll.
  • Old /status fields: I built base afd6af0 and this head on the same fixture in 4 states (running, shadow, alarm, stopped). banner, level, rows, checks, hint and events are identical, apart from the wall-clock stamp, and the old keys come first in the same order. Nothing else reads status_payload(). MQTT does not.
  • Signs: house = grid + solar + battery with grid + import (P1 net_w; the entity goes through meter_invert) and battery + discharge. A stale battery register reads as inv.batt = None, so House, the Batteries tile and the battery's now line show "–" or "No reading", not a number.
  • Ingress: all URLs are relative (status, action, config). There is no url(, @import or external script. The docs link is a plain target=_top link.

Findings (I will push fixes for all of them)

  1. Truthfulness: "Cheap hour" / "Expensive hour" by thirds. Arbitrage buys only in the single cheapest firm point of the plan and sells only in the single dearest one. On the demo day (tools/ux13_demo.py prices), 9 bars are marked "a cheap hour" and 4 "an expensive hour", but the add-on charges in 1 hour and gives in 1. Next to a "Charges from the grid" legend, a home owner reads the other four as charge hours too. The thirds describe the price, not the plan, so the legend and tooltip should say Low price / High price ("low for today" / "high for today").
  2. Truthfulness: trade blocks where the cycle will not trade.
    a. A battery at its ceiling (charge) or at its floor (give) still gets a block, with the text "Charges up to 0.0 kWh from the grid."
    b. The cycle refuses a sale for the whole site when the emptiest pack is at or under arbitrage_soc_floor (soc=self.pack_floor_soc). Home still draws a give block on the other battery.
    c. A negative dearest price is refused (NEGATIVE_PRICE), but Home draws it.
    d. An unknown battery level makes the cycle idle (IDLE_NO_SOC), but Home draws uncapped blocks.
  3. Truthfulness: every refusal reads "The price gap is too small." A plan with fewer than two published (firm) points is refused for that reason, not for the gap. That happens on a feed that is still all carried forward.
  4. Truthfulness: "The batteries step in above 3.5 kW" uses peak_cap_w. Once this month's observed peak is above the cap, the guard's ceiling is that peak (quarter_ceiling_w), so the batteries step in above that figure. The peak card already uses pg.ceiling_w, and the plan list should use it too.
  5. Now: a negative House. grid, solar and battery are read at slightly different moments, so the sum can be below zero. _kw() takes abs(), so the page then shows e.g. "0.3 kW Using", a made-up number. It should show 0 within the idle band, and "–" beyond it.
  6. Control: double toggle. setControl() reads the live state and then toggles. Two confirms in quick succession (the second before the first /action lands) both read "running" and toggle twice. It needs an in-flight guard.

I will also run the two mutations you asked for, and click through the page in headless Chromium, after the fixes.

## Review of UX-13 P1 (head f8d63cc) **Verified, no finding** - `plan.arbitrage()` gets the cycle's own inputs: `arbitrage.guard_state(c.peak_guard)`, `threshold_eur_kwh(c.o)`, `{NO_WEATHER}`, on `c.fresh_tariff_plan(now)`. Home trades only on the fresh plan, as the cycle does. The static profile only colours the bars. - Read-only: after `status_payload()` the controller has no new or rebound attributes. Only the display cache `home_prices` fills in place. The plan's points are unchanged. - Cost: `status_payload()` takes 2.8 ms (two batteries, 48 points) in the CI image. That is fine at a 3 s poll. - Old `/status` fields: I built base afd6af0 and this head on the same fixture in 4 states (running, shadow, alarm, stopped). `banner`, `level`, `rows`, `checks`, `hint` and `events` are identical, apart from the wall-clock stamp, and the old keys come first in the same order. Nothing else reads `status_payload()`. MQTT does not. - Signs: `house = grid + solar + battery` with grid + import (P1 `net_w`; the entity goes through `meter_invert`) and battery + discharge. A stale battery register reads as `inv.batt = None`, so House, the Batteries tile and the battery's now line show "–" or "No reading", not a number. - Ingress: all URLs are relative (`status`, `action`, `config`). There is no `url(`, `@import` or external script. The docs link is a plain `target=_top` link. **Findings (I will push fixes for all of them)** 1. **Truthfulness: "Cheap hour" / "Expensive hour" by thirds.** Arbitrage buys only in the single cheapest *firm* point of the plan and sells only in the single dearest one. On the demo day (`tools/ux13_demo.py` prices), 9 bars are marked "a cheap hour" and 4 "an expensive hour", but the add-on charges in 1 hour and gives in 1. Next to a "Charges from the grid" legend, a home owner reads the other four as charge hours too. The thirds describe the price, not the plan, so the legend and tooltip should say **Low price** / **High price** ("low for today" / "high for today"). 2. **Truthfulness: trade blocks where the cycle will not trade.** a. A battery at its ceiling (charge) or at its floor (give) still gets a block, with the text "Charges up to 0.0 kWh from the grid." b. The cycle refuses a sale for the whole site when the *emptiest* pack is at or under `arbitrage_soc_floor` (`soc=self.pack_floor_soc`). Home still draws a give block on the other battery. c. A negative dearest price is refused (`NEGATIVE_PRICE`), but Home draws it. d. An unknown battery level makes the cycle idle (`IDLE_NO_SOC`), but Home draws uncapped blocks. 3. **Truthfulness: every refusal reads "The price gap is too small."** A plan with fewer than two published (firm) points is refused for that reason, not for the gap. That happens on a feed that is still all carried forward. 4. **Truthfulness: "The batteries step in above 3.5 kW" uses `peak_cap_w`.** Once this month's observed peak is above the cap, the guard's ceiling is that peak (`quarter_ceiling_w`), so the batteries step in above that figure. The peak card already uses `pg.ceiling_w`, and the plan list should use it too. 5. **Now: a negative House.** grid, solar and battery are read at slightly different moments, so the sum can be below zero. `_kw()` takes `abs()`, so the page then shows e.g. "0.3 kW Using", a made-up number. It should show 0 within the idle band, and "–" beyond it. 6. **Control: double toggle.** `setControl()` reads the live state and then toggles. Two confirms in quick succession (the second before the first `/action` lands) both read "running" and toggle twice. It needs an in-flight guard. I will also run the two mutations you asked for, and click through the page in headless Chromium, after the fixes.
admin added 1 commit 2026-09-28 01:18:35 +02:00
UX-13 P1 review: Home draws only what the cycle would do
checks / unit (pull_request) Successful in 9m36s
infra-pin / fresh (pull_request) Successful in 1s
fad73d4012
- Trade blocks: none on a battery with no room, no sale at a negative price
  or while the emptiest battery is at arbitrage's floor, nothing when a
  level is unknown. A give after a charge may use what the charge put in.
- The refusal line names the real reason: prices not published vs gap.
- The plan's peak line uses the guard's ceiling (this month's peak once
  above the cap), as the peak card does.
- House below zero: 0 W inside the idle band, no figure beyond it.
- Price bars are "Low price" / "High price": the add-on trades in one hour.
- Control toggle: one request in flight at a time.
- test_home: the new cases; the house-sign mutation now goes red.

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

Fixes for 1867, pushed as fad73d4

  1. Low price / High price. The legend now reads "Low price" / "High price" and the tooltip "…per kWh, low for today". The kind keys are unchanged, and the docstring gives the reason.
  2. Trade blocks (trade_runs):
    • No block under MIN_KWH (0.05).
    • No give block at a negative price, or while the emptiest battery is at the floor.
    • Nothing is drawn when a level is unknown, and a line "a battery level is unknown" says why.
    • The legs are taken in time order, so a give after a charge may use what the charge put in. On the demo day, Battery B's give therefore goes from 0.81 to 2.5 kWh. It is still an "up to" figure.
  3. Refusals. refusal_key() follows TariffPlan.arbitrage()'s own order: peak, then no prices, then under two firm points ("the prices are not published"), then the gap.
  4. Peak line. The plan's peak line uses pg.ceiling_w or cap, as the peak card does.
  5. House below zero. It shows 0 W inside the idle band (−50 W). Beyond that there is no figure and the tile says "Readings do not add up".
  6. Double toggle. An in-flight guard in setControl().

Tests. The new cases are in test_home §1–2. Every test_*.py passes in the CI image, and test_messages part 8 covers the new sentences.

Mutations (test_home):

  • House + batt → - batt: red (the PR's own suite stayed green on this one).
  • held = False (blocks on red): red.
  • Floor gate removed: red.
  • MIN_KWH gate removed: red.

Headless Chromium (Playwright 1.48). tools/ux13_demo.py was served under a fake ingress prefix /api/hassio_ingress/Tok3n/. I ran the running, shadow and alarm states, each in light at 1200 px and dark at 390 px:

  • No page or console errors, and no request outside the prefix.
  • The config link resolves under the prefix and returns 200.
  • Tabs: ArrowRight goes to #batteries. The chart shows a tooltip on keyboard focus and moves with ArrowRight/End. Hover works on blocks.
  • "Details for installers" shows 14 rows and 18 checks.
  • Stop → Start through the dialog round-trips. Two quick confirms toggle once.
  • More actions: start cycle → "Battery maintenance: drain", stop cycle → "idle".
## Fixes for 1867, pushed as fad73d4 1. **Low price / High price.** The legend now reads "Low price" / "High price" and the tooltip "…per kWh, low for today". The `kind` keys are unchanged, and the docstring gives the reason. 2. **Trade blocks** (`trade_runs`): - No block under `MIN_KWH` (0.05). - No give block at a negative price, or while the emptiest battery is at the floor. - Nothing is drawn when a level is unknown, and a line "a battery level is unknown" says why. - The legs are taken in time order, so a give after a charge may use what the charge put in. On the demo day, Battery B's give therefore goes from 0.81 to 2.5 kWh. It is still an "up to" figure. 3. **Refusals.** `refusal_key()` follows `TariffPlan.arbitrage()`'s own order: peak, then no prices, then under two firm points ("the prices are not published"), then the gap. 4. **Peak line.** The plan's peak line uses `pg.ceiling_w or cap`, as the peak card does. 5. **House below zero.** It shows 0 W inside the idle band (−50 W). Beyond that there is no figure and the tile says "Readings do not add up". 6. **Double toggle.** An in-flight guard in `setControl()`. **Tests.** The new cases are in test_home §1–2. Every `test_*.py` passes in the CI image, and test_messages part 8 covers the new sentences. **Mutations** (test_home): - House `+ batt` → `- batt`: **red** (the PR's own suite stayed green on this one). - `held = False` (blocks on red): **red**. - Floor gate removed: **red**. - `MIN_KWH` gate removed: **red**. **Headless Chromium** (Playwright 1.48). `tools/ux13_demo.py` was served under a fake ingress prefix `/api/hassio_ingress/Tok3n/`. I ran the running, shadow and alarm states, each in light at 1200 px and dark at 390 px: - No page or console errors, and no request outside the prefix. - The `config` link resolves under the prefix and returns 200. - Tabs: ArrowRight goes to `#batteries`. The chart shows a tooltip on keyboard focus and moves with ArrowRight/End. Hover works on blocks. - "Details for installers" shows 14 rows and 18 checks. - Stop → Start through the dialog round-trips. Two quick confirms toggle once. - More actions: start cycle → "Battery maintenance: drain", stop cycle → "idle".
admin merged commit a36dca60dd into release/1.0 2026-09-28 01:29:31 +02:00
Author
Owner

Every point of 1867 is fixed in fad73d4 (see 1870). CI was green on the head (checks / unit and infra-pin / fresh). I merged with head_commit_id pinned, and git ls-remote shows release/1.0 = a36dca6. The post-merge infra-pin / fresh (push) is green, and no pin bump was needed.

Follow-ups:
(a) The per-battery charge amount does not include the SAFETY-45 headroom clamp. It is an upper bound ("up to"), so it is honest, but it can be well above what the peak limit allows.
(b) Home does not project the maintenance restart cooldown after an abort.
(c) tools/ux13_demo.py sets a peak guard while meter_source is off, so its shadow and running pictures show trades that a real install with that setting would refuse.

Every point of 1867 is fixed in fad73d4 (see 1870). CI was green on the head (checks / unit and infra-pin / fresh). I merged with `head_commit_id` pinned, and `git ls-remote` shows release/1.0 = a36dca6. The post-merge `infra-pin / fresh (push)` is green, and no pin bump was needed. Follow-ups: (a) The per-battery charge amount does not include the SAFETY-45 headroom clamp. It is an upper bound ("up to"), so it is honest, but it can be well above what the peak limit allows. (b) Home does not project the maintenance restart cooldown after an abort. (c) `tools/ux13_demo.py` sets a peak guard while `meter_source` is off, so its shadow and running pictures show trades that a real install with that setting would refuse.
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#144