UX-13 P2: the Installer settings view #145

Merged
admin merged 4 commits from UX-13-p2-installer into release/1.0 2026-09-28 03:54:24 +02:00
Owner

Card UX-13 (6ab979852f3eca02fb40f69e), phase P2: the Installer settings view.

What changes

  • Settings tab = Installer view (app/static/settings.js). It is drawn entirely from app/uilayout.py through the new app/uipage.py (labels from the Supervisor's translations, ranges and choices from its schema, words from app/ui_text). It has:
    • a search box;
    • six collapsible groups: Meter · Inverters and batteries · Tariff and prices · Safety limits · Maintenance · Control tuning;
    • "n to fix / n to check / n changed" badges and an "Applies after a restart" chip on each group;
    • radio cards (meter source), segmented choices, sliders with units (the peak limit in kW, stored in W), entity pickers with search, the matching kind first and the live reading, inverter cards (+ Add an inverter) and solar rows with "Reads negative";
    • locked values behind "Change" (every control-tuning value, the safety stops, the peak reserve);
    • option keys in small grey text.
  • Save path. The page posts its page model to the existing config/validate and config/save. The add-on turns the model into options with uimodel.ui_to_options() before the shipped validator runs, so nothing it refuses is saved. No route was added (test_selfconfig part 7 is unchanged).
    • Refusals, warnings and info are placed beside the field they name (uipage.anchors), including the right inverter card; a sentence that names no setting on the page goes in an "Other messages" box.
    • A value equal to the stored one is written as stored, in the stored key order, so a save that changed nothing writes a byte-identical blob (the browser turns -10.0 into -10; this undoes that).
    • Save bar → the existing compare-and-swap save → "Saved, not running yet" with Restart now → the restart dialog, which names 0 W. A stale save (409) names the fields that changed elsewhere.
  • Region: the choice reads "Flanders, Fluvius Antwerpen, 2026 figures" with a "2026 figures" chip. Wallonia and Brussels are listed, disabled, as "no preset yet" (TARIFF-22). The preset fills the levies figure, read-only, until you choose "own".
  • Classic settings link keeps /config reachable until P5.
  • uilayout: mirror (a contract with no feed also sets the fallback price) and fills (the region preset). The name and capacity fields show only on a listed roster. uimodel: DeriveError carries a code and a field, and a list that replaces the one flat inverter clears the flat power request entity (CFG-04 9e).

P1 test findings

  • (a) Home says "Going to 0 W" until each inverter's last accepted power request is 0 W, then "Held at 0 W". This covers the alarm, a stop and shadow (homeview.at_zero).
  • (b) The trade blocks and the price line now use the feed's own points: "Lowest published price, €0.15 per kWh." and "Today: lowest €0.15 at 13:45, highest …". "The list" is gone. The hour bars' tooltips say "hour average" when an hour has several points.
  • (c) On a timeout the entity-list WARNING now reads "Reading the entity list failed: no answer within 10 seconds." For any other error it gives the error's text, or its class name if the text is empty.

Tests

  • New test_settings.py:
    • every group and every mapped field (82 fields: meter 14, inverters 10 incl. 8 card fields, tariff 22, safety 17, maintenance 9, tuning 10) with label, schema range and choices;
    • the region year and the "no preset yet" entries; the locked fields;
    • settings.js names no option key, sets no innerHTML and uses relative URLs only;
    • over real HTTP with a fake Supervisor: the unchanged-save identical blob, a two-field edit writing exactly two keys, every known blob byte for byte through a browser-style JSON round trip, inline refusal placement (card 2's blank entity, two cards on one entity, the peak reserve against each card's cut-out level, a DeriveError), a refused save writing nothing, and a 409 naming the fields;
    • the STYLE scan of the view's words (one sentence ≤25 words, names ≤6).
  • test_home covers (a) and (b), including a quarter-hourly feed; test_haentities covers (c); test_uilayout also bars option keys from uipage.py.
  • All 43 test_*.py pass locally in goodwe-addon-checks:1 after merging release/1.0 (28a49cc).

Headless click-through (TEST-23 not used, it was reserved for SAFETY-73)

tools/ux13_demo.py now serves a stored blob, a schema read off config.yaml and en.yaml. Checked with Playwright Chromium in a container: 34 of 34 passed.

  • Six groups; every mapped field rendered; search ("peak", and no match).
  • Locked gain → Change → slider. Region preset/year/disabled levies, and "own" makes it editable.
  • An edit → save bar "1 change not saved" and badge "1 changed".
  • A new card's blank power request entity → red beside that card's field with "2 to fix". The refused save wrote nothing.
  • The picker shows "-10 W" live, search lists kW entities, and picking a kW sensor gives a "reads in kW" warning and "1 to check".
  • Save 4.2 kW → exactly peak_cap_w: 4200 written → banner → restart dialog says 0 W. An unchanged save writes an identical blob.
  • Home plan uses "Lowest published price" and "Today: lowest".
  • No side scroll at 390 px in light and dark mode; touch targets ≥40 px; no JS errors; no request left the add-on.

Not verified on real Home Assistant or the Supervisor: the live Supervisor schema/translations shape is assumed from test_selfconfig's fixture and range_of()'s probe notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p

Card UX-13 (6ab979852f3eca02fb40f69e), phase P2: the Installer settings view. ## What changes - **Settings tab = Installer view** (`app/static/settings.js`). It is drawn entirely from `app/uilayout.py` through the new `app/uipage.py` (labels from the Supervisor's translations, ranges and choices from its schema, words from `app/ui_text`). It has: - a search box; - six collapsible groups: Meter · Inverters and batteries · Tariff and prices · Safety limits · Maintenance · Control tuning; - "n to fix / n to check / n changed" badges and an "Applies after a restart" chip on each group; - radio cards (meter source), segmented choices, sliders with units (the peak limit in kW, stored in W), entity pickers with search, the matching kind first and the live reading, inverter cards (+ Add an inverter) and solar rows with "Reads negative"; - locked values behind "Change" (every control-tuning value, the safety stops, the peak reserve); - option keys in small grey text. - **Save path.** The page posts its **page model** to the existing `config/validate` and `config/save`. The add-on turns the model into options with `uimodel.ui_to_options()` **before** the shipped validator runs, so nothing it refuses is saved. **No route was added** (test_selfconfig part 7 is unchanged). - Refusals, warnings and info are placed beside the field they name (`uipage.anchors`), including the right inverter card; a sentence that names no setting on the page goes in an "Other messages" box. - A value equal to the stored one is written as stored, in the stored key order, so **a save that changed nothing writes a byte-identical blob** (the browser turns -10.0 into -10; this undoes that). - Save bar → the existing compare-and-swap save → "Saved, not running yet" with **Restart now** → the restart dialog, which names 0 W. A stale save (409) names the fields that changed elsewhere. - **Region:** the choice reads "Flanders, Fluvius Antwerpen, 2026 figures" with a "2026 figures" chip. Wallonia and Brussels are listed, disabled, as "no preset yet" (TARIFF-22). The preset fills the levies figure, read-only, until you choose "own". - **Classic settings** link keeps `/config` reachable until P5. - `uilayout`: `mirror` (a contract with no feed also sets the fallback price) and `fills` (the region preset). The name and capacity fields show only on a listed roster. `uimodel`: `DeriveError` carries a code and a field, and a list that replaces the one flat inverter clears the flat power request entity (CFG-04 9e). ## P1 test findings - **(a)** Home says **"Going to 0 W"** until each inverter's last accepted power request is 0 W, then "Held at 0 W". This covers the alarm, a stop and shadow (`homeview.at_zero`). - **(b)** The trade blocks and the price line now use the feed's own points: "Lowest published price, €0.15 per kWh." and "Today: lowest €0.15 at 13:45, highest …". "The list" is gone. The hour bars' tooltips say "hour average" when an hour has several points. - **(c)** On a timeout the entity-list WARNING now reads "Reading the entity list failed: no answer within 10 seconds." For any other error it gives the error's text, or its class name if the text is empty. ## Tests - **New `test_settings.py`:** - every group and every mapped field (82 fields: meter 14, inverters 10 incl. 8 card fields, tariff 22, safety 17, maintenance 9, tuning 10) with label, schema range and choices; - the region year and the "no preset yet" entries; the locked fields; - `settings.js` names no option key, sets no innerHTML and uses relative URLs only; - over real HTTP with a fake Supervisor: the unchanged-save identical blob, a two-field edit writing exactly two keys, every known blob byte for byte through a browser-style JSON round trip, inline refusal placement (card 2's blank entity, two cards on one entity, the peak reserve against each card's cut-out level, a DeriveError), a refused save writing nothing, and a 409 naming the fields; - the STYLE scan of the view's words (one sentence ≤25 words, names ≤6). - `test_home` covers (a) and (b), including a quarter-hourly feed; `test_haentities` covers (c); `test_uilayout` also bars option keys from `uipage.py`. - All 43 `test_*.py` pass locally in `goodwe-addon-checks:1` after merging release/1.0 (28a49cc). ## Headless click-through (TEST-23 not used, it was reserved for SAFETY-73) `tools/ux13_demo.py` now serves a stored blob, a schema read off config.yaml and en.yaml. Checked with Playwright Chromium in a container: **34 of 34** passed. - Six groups; every mapped field rendered; search ("peak", and no match). - Locked gain → Change → slider. Region preset/year/disabled levies, and "own" makes it editable. - An edit → save bar "1 change not saved" and badge "1 changed". - A new card's blank power request entity → red beside that card's field with "2 to fix". The refused save wrote nothing. - The picker shows "-10 W" live, search lists kW entities, and picking a kW sensor gives a "reads in kW" warning and "1 to check". - Save 4.2 kW → exactly `peak_cap_w: 4200` written → banner → restart dialog says 0 W. An unchanged save writes an identical blob. - Home plan uses "Lowest published price" and "Today: lowest". - No side scroll at 390 px in light and dark mode; touch targets ≥40 px; no JS errors; no request left the add-on. Not verified on real Home Assistant or the Supervisor: the live Supervisor schema/translations shape is assumed from `test_selfconfig`'s fixture and `range_of()`'s probe notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
admin added 3 commits 2026-09-28 03:03:15 +02:00
- Settings tab: the Installer view (app/static/settings.js), drawn from
  app/uilayout.py through the new app/uipage.py: search, six collapsible
  groups with "n to fix / n to check / n changed" badges and an "applies
  after a restart" chip, radio cards, segmented choices, sliders with units
  (the peak limit in kW), entity pickers with search and the live reading,
  inverter cards, solar rows, locked measured values behind "Change", and
  the add-on's refusals, warnings and info beside the field they name.
- Save path: the page posts its model to the existing config/validate and
  config/save; uimodel turns it into options before the shipped validator,
  so nothing it refuses is saved. No route added. A value equal to the
  stored one is written as stored, so a save that changed nothing writes an
  identical blob. Save bar, then the restart dialog that names 0 W.
- The region choice shows its preset year; Wallonia and Brussels are listed
  as having no preset yet (TARIFF-22). "Classic settings" keeps /config.
- P1 test findings: (a) "Going to 0 W" until the last accepted power request
  is 0 W; (b) the trade blocks and the price line name the feed's own points
  ("Lowest published price"), the hour bars say "hour average"; (c) the entity
  list's WARNING gives a reason on a timeout.
- test_settings.py; test_home, test_haentities, test_uilayout extended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
UX-13 P2: test_settings reads fields through named_absence
checks / unit (pull_request) Successful in 10m15s
infra-pin / fresh (pull_request) Successful in 2s
1752adf6e6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
Author
Owner

Review of UX-13 P2 (head 1752adf)

Verified, no finding

  • Save round trip (15 cases, through uipage.to_options and the validator, then the real Supervisor 2026.08.0 AppOptions validator from ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0 against this config.yaml). The cases: meter source changed and changed back (identical blob); one card's max_w edited in a list (only inverters changes); capacity cleared on one card (the key is dropped, not written as null); region "own" with a custom levies figure, own→Flanders, and own→Flanders→own; static_profile as "not json", odd windows, and a fixed profile with trailing spaces, each plus an unrelated edit (kept byte for byte); a fixed price edit; pv_entity→one row, two rows, a list back to one row, and pv_entity with a stray flip plus a second row (the stray flip is kept); a malformed roster plus another edit (kept). Key order is kept and no key is dropped in any case. The Supervisor accepts every blob our validator accepts. The only coercions are int→float (capacity_kwh 10.0 comes back from the browser as 10, target_grid_w 0 from the defaults), which the Supervisor stores as floats. No null reaches the blob: capacity_kwh is the only optional number, and a blank one is dropped.
  • A refused save writes nothing: checked in the browser (422) and by mutation.
  • 409 stale compares against a fresh read of the Supervisor, taken immediately before the write (selfconfig.save).
  • Locked values: 20 at load, with no input in any of them until "Change" is pressed.
  • Keys rule: hard-coding peak_cap_w in settings.js, 'meter_source' in single quotes, or a key compared in uipage.py each turns test_settings or test_uilayout red.
  • Mutations: dropping the key-order preservation (2 red), dropping the stored-value reuse (1 red), making a refused save write (red in test_settings and test_selfconfig), and ignoring the fingerprint (red) all fail. One survived: a DeriveError on the save path that writes still passes (see 8).
  • 390 px: scrollWidth is 390 with every group open. Search finds rows by label, by key and by a card's setpoint_entity, and shows the no-match line.
  • After a save the page says "Saved, not running yet", and the restart dialog names 0 W. The classic /config answers 200. The /status keys are unchanged, apart from the P1 additions.

Findings

  1. Discard after a stale save clobbers the other surface (save safety). Page open → the Configuration tab sets peak_cap_w 5000 → the page saves → 409 (correct) → the banner says "discard to load theirs" → Discard → any edit → Save: the blob is written with peak_cap_w 4500, and there is no 409. Discard resets to the old ORIG but keeps the new fingerprint. It must reload /config/model.
  2. Adding and then removing an inverter on a flat site leaves inverter_shape: "list". The name and capacity rows stay visible. With no other edit there is no save bar. With any other edit, the save is refused with "inverters[0] … has no name", which is about something the user never asked for. Nothing is written, but the site cannot be saved until Discard. When the list is back to one card, the stored shape should come back.
  3. An "only if held" choice cannot be chosen again once left. A site with odd tariff windows (contract, or fallback beside a dynamic source): Fixed, then back. "Other time windows" is no longer offered, and dynamic→fixed→dynamic leaves the fallback on "One price" with "Enter a price per kWh". Only Discard recovers it. The stored value should stay offered.
  4. Unlocked values stay unlocked after a save (UNLOCKED is cleared only by Discard). After save→reload, peak_soc_floor still shows its input.
  5. (a) "Held at 0 W" before any accepted request. at_zero() uses standing_w(), which falls back to target (0.0) while last_write is None. Right after a start, or while Home Assistant refuses every write, Home says "Held at 0 W" although no 0 W request was ever accepted, which is P1 finding (a) again. With last_write is not None it cannot get stuck: the heartbeat writes every heartbeat_s, stopped or not (run_control), so "Going to 0 W" turns into "Held" on the first accepted write, and it stays "Going" only while writes really fail.
  6. The unit on a label is wrong for the kW slider. The label comes from translations: "Peak limit (W)" sits beside "3.5 kW". Other rows repeat their unit ("Peak reserve (%)" → "20 %"). The page already shows the unit next to the control, so a field that declares a unit should drop the translation's trailing "(unit)".
  7. Search by a derived key misses when its field is hidden. static_profile is shown only under the fallback (ui.fallback), which is hidden unless the contract is dynamic. On a fixed-price site, searching "static_profile" finds nothing. The contract choice mirrors into it, so the contract should list it too.
  8. Test gap: no check that a save refused by ui_to_options (422, DeriveError) writes nothing. The mutation survives.

Follow-ups (not blocking P2)

  • Several translation help texts name raw values ("'off' uses…", "only homewizard_local…", "'auto' does this…") next to choices that now have plain names. P5 text pass.
  • The stale banner lists field names but not the two values; the classic page shows "theirs → mine". Worth adding when P4 reuses the save bar.

I will push fixes for 1-8 on this branch.

## Review of UX-13 P2 (head 1752adf) **Verified, no finding** - **Save round trip (15 cases, through `uipage.to_options` and the validator, then the real Supervisor 2026.08.0 `AppOptions` validator from `ghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0` against this `config.yaml`).** The cases: meter source changed and changed back (identical blob); one card's `max_w` edited in a list (only `inverters` changes); capacity cleared on one card (the key is dropped, not written as null); region "own" with a custom levies figure, own→Flanders, and own→Flanders→own; `static_profile` as "not json", odd windows, and a fixed profile with trailing spaces, each plus an unrelated edit (kept byte for byte); a fixed price edit; `pv_entity`→one row, two rows, a list back to one row, and `pv_entity` with a stray flip plus a second row (the stray flip is kept); a malformed roster plus another edit (kept). Key order is kept and no key is dropped in any case. The Supervisor accepts every blob our validator accepts. The only coercions are int→float (`capacity_kwh` 10.0 comes back from the browser as 10, `target_grid_w` 0 from the defaults), which the Supervisor stores as floats. No null reaches the blob: `capacity_kwh` is the only optional number, and a blank one is dropped. - **A refused save writes nothing:** checked in the browser (422) and by mutation. - **409 stale** compares against a fresh read of the Supervisor, taken immediately before the write (`selfconfig.save`). - **Locked values:** 20 at load, with no input in any of them until "Change" is pressed. - **Keys rule:** hard-coding `peak_cap_w` in settings.js, `'meter_source'` in single quotes, or a key compared in uipage.py each turns test_settings or test_uilayout red. - **Mutations:** dropping the key-order preservation (2 red), dropping the stored-value reuse (1 red), making a refused save write (red in test_settings and test_selfconfig), and ignoring the fingerprint (red) all fail. One survived: a `DeriveError` on the *save* path that writes still passes (see 8). - **390 px:** `scrollWidth` is 390 with every group open. Search finds rows by label, by key and by a card's `setpoint_entity`, and shows the no-match line. - **After a save** the page says "Saved, not running yet", and the restart dialog names 0 W. The classic `/config` answers 200. The `/status` keys are unchanged, apart from the P1 additions. **Findings** 1. **Discard after a stale save clobbers the other surface (save safety).** Page open → the Configuration tab sets `peak_cap_w` 5000 → the page saves → 409 (correct) → the banner says "discard to load theirs" → Discard → any edit → Save: the blob is written with `peak_cap_w` **4500**, and there is no 409. Discard resets to the *old* `ORIG` but keeps the *new* fingerprint. It must reload `/config/model`. 2. **Adding and then removing an inverter on a flat site leaves `inverter_shape: "list"`.** The name and capacity rows stay visible. With no other edit there is no save bar. With any other edit, the save is refused with "inverters[0] … has no name", which is about something the user never asked for. Nothing is written, but the site cannot be saved until Discard. When the list is back to one card, the stored shape should come back. 3. **An "only if held" choice cannot be chosen again once left.** A site with odd tariff windows (contract, or fallback beside a dynamic source): Fixed, then back. "Other time windows" is no longer offered, and dynamic→fixed→dynamic leaves the fallback on "One price" with "Enter a price per kWh". Only Discard recovers it. The stored value should stay offered. 4. **Unlocked values stay unlocked after a save** (`UNLOCKED` is cleared only by Discard). After save→reload, `peak_soc_floor` still shows its input. 5. **(a) "Held at 0 W" before any accepted request.** `at_zero()` uses `standing_w()`, which falls back to `target` (0.0) while `last_write` is None. Right after a start, or while Home Assistant refuses every write, Home says "Held at 0 W" although no 0 W request was ever accepted, which is P1 finding (a) again. With `last_write is not None` it cannot get stuck: the heartbeat writes every `heartbeat_s`, stopped or not (`run_control`), so "Going to 0 W" turns into "Held" on the first accepted write, and it stays "Going" only while writes really fail. 6. **The unit on a label is wrong for the kW slider.** The label comes from translations: "Peak limit (W)" sits beside "3.5 kW". Other rows repeat their unit ("Peak reserve (%)" → "20 %"). The page already shows the unit next to the control, so a field that declares a unit should drop the translation's trailing "(unit)". 7. **Search by a derived key misses when its field is hidden.** `static_profile` is shown only under the fallback (`ui.fallback`), which is hidden unless the contract is dynamic. On a fixed-price site, searching "static_profile" finds nothing. The contract choice mirrors into it, so the contract should list it too. 8. **Test gap:** no check that a save refused by `ui_to_options` (422, `DeriveError`) writes nothing. The mutation survives. **Follow-ups (not blocking P2)** - Several translation help texts name raw values ("'off' uses…", "only homewizard_local…", "'auto' does this…") next to choices that now have plain names. P5 text pass. - The stale banner lists field names but not the two values; the classic page shows "theirs → mine". Worth adding when P4 reuses the save bar. I will push fixes for 1-8 on this branch.
admin added 1 commit 2026-09-28 03:43:49 +02:00
UX-13 P2 review fixes (PR #145, comment 1880)
checks / unit (pull_request) Successful in 10m15s
infra-pin / fresh (pull_request) Successful in 2s
7be09197d4
1. Discard reloads /config/model: after a stale save the page kept the
   new fingerprint with the old values, so Discard + Save overwrote the
   other surface's change without a 409.
2. Removing cards back to one restores the stored inverter shape, so a
   flat site stays flat after add + remove.
3. A choice the page can only keep ("Other time windows") stays offered
   while it is the stored value.
4. A load locks every locked value again.
5. at_zero() needs an accepted 0 W write; before the first one Home says
   "Going to 0 W" (the heartbeat writes every heartbeat_s, so it clears).
6. A field with a unit drops the translation's "(W)" from its label: the
   peak limit read "Peak limit (W)" beside a kW value.
7. A mirrored choice lists the mirrored field's keys, so a search for
   static_profile finds the contract on a fixed-price site.
8. test_settings: a save refused by ui_to_options writes nothing.

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

Fixes for 1880, pushed as 7be0919 (on the branch; git ls-remote confirms)

  1. Discard after a stale save now reloads /config/model (fresh values and fingerprint). Browser re-run: the other surface sets 5000 → 409 → Discard shows 5 kW → an edit and Save → stored peak_cap_w stays 5000.
  2. Add and then remove an inverter on a flat site: back at one card, the stored shape returns. The name and capacity rows hide, there is no save bar, and a later save of auto_start changes only auto_start.
  3. Held-only choices: choicesOf also offers the stored value. With odd windows, dynamic→fixed→dynamic offers "Other time windows" again. Choosing it clears the error and the save bar, and the blob keeps static_profile byte for byte.
  4. Relock: load() clears UNLOCKED, so after a save the locked values show no input.
  5. (a): at_zero() is now last_write is not None and |last_write| < 1. The make() fixture in test_home now takes wrote= (default 0.0 accepted). A new check shows a stopped site with no accepted write reads "Going to 0 W". The demo sets 0 W accepted for shadow and alarm.
  6. Labels: a field with a unit drops a short "(W)"-style part from the translation ("(optional)" stays). The label now reads "Peak limit" beside "4.7 kW". test_settings checks that no field with a unit keeps one.
  7. Search: writes now includes a mirrored field's keys. On a fixed-price site, "static_profile" finds the contract.
  8. Test: test_settings part 4 now checks that a save refused by ui_to_options answers 422 with its sentence and writes nothing. That is the mutation that survived.

All 43 test_*.py pass in goodwe-addon-ci. The browser re-run shows 390 px at scrollWidth 390 and no page errors. The two follow-ups in 1880 (raw values in translation help, the stale banner without values) stay open for P4/P5.

## Fixes for 1880, pushed as 7be0919 (on the branch; `git ls-remote` confirms) 1. **Discard after a stale save** now reloads `/config/model` (fresh values and fingerprint). Browser re-run: the other surface sets 5000 → 409 → Discard shows 5 kW → an edit and Save → stored `peak_cap_w` stays **5000**. 2. **Add and then remove an inverter on a flat site:** back at one card, the stored shape returns. The name and capacity rows hide, there is no save bar, and a later save of `auto_start` changes only `auto_start`. 3. **Held-only choices:** `choicesOf` also offers the stored value. With odd windows, dynamic→fixed→dynamic offers "Other time windows" again. Choosing it clears the error and the save bar, and the blob keeps `static_profile` byte for byte. 4. **Relock:** `load()` clears `UNLOCKED`, so after a save the locked values show no input. 5. **(a):** `at_zero()` is now `last_write is not None and |last_write| < 1`. The `make()` fixture in test_home now takes `wrote=` (default 0.0 accepted). A new check shows a stopped site with no accepted write reads "Going to 0 W". The demo sets 0 W accepted for shadow and alarm. 6. **Labels:** a field with a unit drops a short "(W)"-style part from the translation ("(optional)" stays). The label now reads "Peak limit" beside "4.7 kW". test_settings checks that no field with a unit keeps one. 7. **Search:** `writes` now includes a mirrored field's keys. On a fixed-price site, "static_profile" finds the contract. 8. **Test:** test_settings part 4 now checks that a save refused by `ui_to_options` answers 422 with its sentence and writes nothing. That is the mutation that survived. All 43 `test_*.py` pass in `goodwe-addon-ci`. The browser re-run shows 390 px at `scrollWidth` 390 and no page errors. The two follow-ups in 1880 (raw values in translation help, the stale banner without values) stay open for P4/P5.
admin merged commit 515c11b380 into release/1.0 2026-09-28 03:54:24 +02:00
Author
Owner

Every point of 1880 is fixed in 7be0919 (see 1883). 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 = 515c11b (parents 28a49cc and 7be0919). The post-merge infra-pin / fresh (push) is green, and no pin bump was needed.

Follow-ups: (a) several translation help texts still name raw values ('off', homewizard_local, 'auto') next to choices that now have plain names; that is for the P5 text pass. (b) The stale banner lists the fields but not theirs → mine, which the classic page shows. (c) Not checked: real HA or a live Supervisor round trip. The Supervisor 2026.08.0 AppOptions validator run offline accepted every blob.

Every point of 1880 is fixed in 7be0919 (see 1883). 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 = 515c11b (parents 28a49cc and 7be0919). The post-merge `infra-pin / fresh (push)` is green, and no pin bump was needed. Follow-ups: (a) several translation help texts still name raw values ('off', homewizard_local, 'auto') next to choices that now have plain names; that is for the P5 text pass. (b) The stale banner lists the fields but not theirs → mine, which the classic page shows. (c) Not checked: real HA or a live Supervisor round trip. The Supervisor 2026.08.0 `AppOptions` validator run offline accepted every blob.
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#145