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.
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
- 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
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
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_w4500, and there is no 409. Discard resets to the oldORIG but keeps the new fingerprint. It must reload /config/model.
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.
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.
Unlocked values stay unlocked after a save (UNLOCKED is cleared only by Discard). After save→reload, peak_soc_floor still shows its input.
(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.
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)".
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.
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.
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>
Fixes for 1880, pushed as 7be0919 (on the branch; git ls-remote confirms)
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.
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.
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.
Relock:load() clears UNLOCKED, so after a save the locked values show no input.
(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.
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.
Search:writes now includes a mirrored field's keys. On a fixed-price site, "static_profile" finds the contract.
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.02026-09-28 03:54:24 +02:00
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.
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.
Card UX-13 (6ab979852f3eca02fb40f69e), phase P2: the Installer settings view.
What changes
app/static/settings.js). It is drawn entirely fromapp/uilayout.pythrough the newapp/uipage.py(labels from the Supervisor's translations, ranges and choices from its schema, words fromapp/ui_text). It has:config/validateandconfig/save. The add-on turns the model into options withuimodel.ui_to_options()before the shipped validator runs, so nothing it refuses is saved. No route was added (test_selfconfig part 7 is unchanged).uipage.anchors), including the right inverter card; a sentence that names no setting on the page goes in an "Other messages" box./configreachable until P5.uilayout:mirror(a contract with no feed also sets the fallback price) andfills(the region preset). The name and capacity fields show only on a listed roster.uimodel:DeriveErrorcarries 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
homeview.at_zero).Tests
test_settings.py:settings.jsnames no option key, sets no innerHTML and uses relative URLs only;test_homecovers (a) and (b), including a quarter-hourly feed;test_haentitiescovers (c);test_uilayoutalso bars option keys fromuipage.py.test_*.pypass locally ingoodwe-addon-checks:1after merging release/1.0 (28a49cc).Headless click-through (TEST-23 not used, it was reserved for SAFETY-73)
tools/ux13_demo.pynow serves a stored blob, a schema read off config.yaml and en.yaml. Checked with Playwright Chromium in a container: 34 of 34 passed.peak_cap_w: 4200written → banner → restart dialog says 0 W. An unchanged save writes an identical blob.Not verified on real Home Assistant or the Supervisor: the live Supervisor schema/translations shape is assumed from
test_selfconfig's fixture andrange_of()'s probe notes.🤖 Generated with Claude Code
https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
- 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_01LyR88rXETvwhTgx6yHE39p28a49cc, SAFETY-73) into UX-13-p2-installer 0a28f71872Review of UX-13 P2 (head
1752adf)Verified, no finding
uipage.to_optionsand the validator, then the real Supervisor 2026.08.0AppOptionsvalidator fromghcr.io/home-assistant/amd64-hassio-supervisor:2026.08.0against thisconfig.yaml). The cases: meter source changed and changed back (identical blob); one card'smax_wedited in a list (onlyinverterschanges); 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_profileas "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, andpv_entitywith 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_kwh10.0 comes back from the browser as 10,target_grid_w0 from the defaults), which the Supervisor stores as floats. No null reaches the blob:capacity_kwhis the only optional number, and a blank one is dropped.selfconfig.save).peak_cap_win settings.js,'meter_source'in single quotes, or a key compared in uipage.py each turns test_settings or test_uilayout red.DeriveErroron the save path that writes still passes (see 8).scrollWidthis 390 with every group open. Search finds rows by label, by key and by a card'ssetpoint_entity, and shows the no-match line./configanswers 200. The/statuskeys are unchanged, apart from the P1 additions.Findings
peak_cap_w5000 → the page saves → 409 (correct) → the banner says "discard to load theirs" → Discard → any edit → Save: the blob is written withpeak_cap_w4500, and there is no 409. Discard resets to the oldORIGbut keeps the new fingerprint. It must reload/config/model.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.UNLOCKEDis cleared only by Discard). After save→reload,peak_soc_floorstill shows its input.at_zero()usesstanding_w(), which falls back totarget(0.0) whilelast_writeis 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. Withlast_write is not Noneit cannot get stuck: the heartbeat writes everyheartbeat_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.static_profileis 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.ui_to_options(422,DeriveError) writes nothing. The mutation survives.Follow-ups (not blocking P2)
I will push fixes for 1-8 on this branch.
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>Fixes for 1880, pushed as
7be0919(on the branch;git ls-remoteconfirms)/config/model(fresh values and fingerprint). Browser re-run: the other surface sets 5000 → 409 → Discard shows 5 kW → an edit and Save → storedpeak_cap_wstays 5000.auto_startchanges onlyauto_start.choicesOfalso 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 keepsstatic_profilebyte for byte.load()clearsUNLOCKED, so after a save the locked values show no input.at_zero()is nowlast_write is not None and |last_write| < 1. Themake()fixture in test_home now takeswrote=(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.writesnow includes a mirrored field's keys. On a fixed-price site, "static_profile" finds the contract.ui_to_optionsanswers 422 with its sentence and writes nothing. That is the mutation that survived.All 43
test_*.pypass ingoodwe-addon-ci. The browser re-run shows 390 px atscrollWidth390 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.Every point of 1880 is fixed in
7be0919(see 1883). CI was green on the head (checks / unit and infra-pin / fresh). I merged withhead_commit_idpinned, andgit ls-remoteshows release/1.0 =515c11b(parents28a49ccand7be0919). The post-mergeinfra-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
AppOptionsvalidator run offline accepted every blob.