UX-13 phase P0 (card 6ab979852f3eca02fb40f69e): the foundation for the new settings page. No visible UI change. P1 builds the page shell and Home on top of this.
What is in it
Layout tableapp/uilayout.py: screen → group → part → field for all 78 options. That is 67 fields, 11 derived and 0 hidden. Each field has its control, unit, display scale, step, entity kind, show conditions, Simple/wizard placement and "applies after a restart". Ranges stay in the schema (range_of()). Words come from translations and the new app/ui_text/en.json.
static_profile built from a fixed or day/night price;
the region preset for the levies figure: Flanders 2026 only, sourced from price.py. Wallonia and Brussels need sourced figures, so those sites choose "own";
the OPT-11 hook for arbitrage_enabled, which changes nothing yet.
An unchanged derivation writes the stored values back, so the round trip is exact.
Warning tier in validate.py (Note: warn/info). Every logged warning is also a note. config_notes() adds:
an entity Home Assistant does not have;
a kW sensor the add-on reads as watts;
a peak limit below this month's peak.
/config/validate returns these as notes. Refusals are untouched.
GET ha/entities?kind=: the picker's list (id, name, state, unit, device_class) via hass.states(). It is cached for 30 s and times out after 10 s. When HA is down it still answers 200: with the last list and "may be old", or with an empty list that says why.
DECISIONS/UX-13-one-layout-table.md: why UX-02's "no option key in UI code" becomes "only in uilayout.py and uimodel.py", and how that is tested.
Evidence
Round trip (options → model → JSON → options) is identical for:
261 recorded validator calls from the suite at 652097a (scenarios/ux13/, recorded with tools/ux13_validator_calls.py);
the 5 TEST-23 option files;
config.yaml's defaults.
It was also run once on the live options, read-only and not committed.
Old vs new validator over those 261 calls: 158 refused, 0 differ. Passing notes= changes no result.
test_uilayout fails when an option or inverter field has no place in the table (the mutation is checked).
Tests widened on purpose:
test_selfconfig part 7 route list (+GET /ha/entities, read-only asserted);
test_pv's module scan now allows the settings modules, and the decision modules stay excluded;
test_messages scans notes and ui_text (25 words, one sentence).
The full suite passes locally in goodwe-addon-checks:1 (40 files).
UX-13 phase P0 (card 6ab979852f3eca02fb40f69e): the foundation for the new settings page. **No visible UI change.** P1 builds the page shell and Home on top of this.
**What is in it**
- **Layout table** `app/uilayout.py`: screen → group → part → field for all 78 options. That is 67 fields, 11 derived and 0 hidden. Each field has its control, unit, display scale, step, entity kind, show conditions, Simple/wizard placement and "applies after a restart". Ranges stay in the schema (`range_of()`). Words come from translations and the new `app/ui_text/en.json`.
- **Derivation** `app/uimodel.py` (`options_to_ui` / `ui_to_options`), covering:
- one inverter vs `inverters`;
- the solar keys, including the reversed-sign list;
- `static_profile` built from a fixed or day/night price;
- the region preset for the levies figure: Flanders 2026 only, sourced from `price.py`. Wallonia and Brussels need sourced figures, so those sites choose "own";
- the OPT-11 hook for `arbitrage_enabled`, which changes nothing yet.
An unchanged derivation writes the stored values back, so the round trip is exact.
- **Warning tier** in `validate.py` (`Note`: warn/info). Every logged warning is also a note. `config_notes()` adds:
- an entity Home Assistant does not have;
- a kW sensor the add-on reads as watts;
- a peak limit below this month's peak.
`/config/validate` returns these as `notes`. Refusals are untouched.
- **`GET ha/entities?kind=`**: the picker's list (id, name, state, unit, device_class) via `hass.states()`. It is cached for 30 s and times out after 10 s. When HA is down it still answers 200: with the last list and "may be old", or with an empty list that says why.
- **`DECISIONS/UX-13-one-layout-table.md`**: why UX-02's "no option key in UI code" becomes "only in uilayout.py and uimodel.py", and how that is tested.
**Evidence**
- Round trip (options → model → JSON → options) is identical for:
- 261 recorded validator calls from the suite at 652097a (`scenarios/ux13/`, recorded with `tools/ux13_validator_calls.py`);
- the 5 TEST-23 option files;
- config.yaml's defaults.
It was also run once on the live options, read-only and not committed.
- Old vs new validator over those 261 calls: 158 refused, **0 differ**. Passing `notes=` changes no result.
- test_uilayout fails when an option or inverter field has no place in the table (the mutation is checked).
- Tests widened on purpose:
- test_selfconfig part 7 route list (+`GET /ha/entities`, read-only asserted);
- test_pv's module scan now allows the settings modules, and the decision modules stay excluded;
- test_messages scans notes and ui_text (25 words, one sentence).
- The full suite passes locally in `goodwe-addon-checks:1` (40 files).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
The foundation for the new settings page. No visible UI change.
- app/uilayout.py: screen -> group -> part -> field for every option in
config.yaml (67 fields, 11 derived, 0 hidden), with control, unit, display
scale, step, entity kind, show conditions, Simple/wizard placement and
"applies after a restart". Ranges stay in the schema, words in
translations and the new app/ui_text/en.json.
- app/uimodel.py: options_to_ui()/ui_to_options() for the derived keys (one
inverter vs the list, solar rows, static_profile from simple prices, a
year-stamped region preset for the levies figure, and an OPT-11 hook for
arbitrage_enabled that changes nothing yet). Unchanged derivations write
the stored values back, so the round trip is exact.
- validate.py: validate.Note, a warn/info tier. Each logged warning is also
a note when a list is passed; config_notes() adds checks that need live
data (missing entity, a kW sensor read as W, a limit below this month's
peak). /config/validate returns them as "notes". No refusal changed: 261
recorded validator calls, 0 differ (tools/ux13_validator_calls.py).
- GET ha/entities: Home Assistant's entity list for the picker, read through
hass.states(), cached 30 s, 10 s timeout. When HA is down it answers 200
with the old list marked old, or an empty list that says why.
- DECISIONS/UX-13-one-layout-table.md amends UX-02's "no option key in UI
code": keys may appear in uilayout.py and uimodel.py only, both tested.
- Tests: test_uilayout, test_uimodel (round trip over the recorded calls,
the TEST-23 option files and config.yaml defaults), test_haentities, a
warning-tier part in test_validate; test_messages scans notes and
ui_text; test_pv and test_selfconfig part 7 widened on purpose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
tools/ux13_validator_calls.py compare against a git archive of 652097a, run in the CI image: 261 calls, 158 refused, 0 changed. I also ran the recorded calls plus 8 malformed blobs with notes=[] against notes=None: 0 refusal diffs. config_notes() raised on none of them.
Notes cannot block anything. /config/save and startup call the validator without notes, and the /config/validate route only adds a notes field.
GET ha/entities exposure: its exposure is the same as every other route (8099, ingress: true, no ports: mapping, no auth of its own). It returns only entity_id, friendly name, state, unit and device_class. The Supervisor token appears only in the outgoing header and the error texts are constants. It is covered by the route-list test and the no-writer scan.
scenarios/ux13/: I compared it against the live options blob. It holds no live-only value. The only overlaps are defaults (/share/goodwe_prices.csv, sensor.p1_meter_active_power) and 192.168.2.250, which was already in test_p1/test_validate at base.
Test changes: the route list is still exact, test_pv still excludes every decider, and test_messages now covers more text. None of them is weaker.
Mutations: a new option and a new inverter field in config.yaml turn test_uilayout red. Dropping the pv_invert_entities write turns test_uimodel red.
Findings
Blocking: editing the levies with region "own" drops price_taxes_profile._region_encode returns only price_taxes_eur_kwh. Repro: {price_taxes_eur_kwh: 0.1234567, price_taxes_profile: '{"day":0.1}'} → set price_taxes_eur_kwh=0.2 → price_taxes_profile is missing from the output. That hits exactly the Wallonia/Brussels sites, which must choose "own". It also contradicts uilayout.DERIVED ("own keeps it").
Blocking: editing any card of an inverters list drops the stored flat keys.soc_entity, batt_entity, batt_invert, max_w, bms_soc_floor and setpoint_entity all disappear from the blob. The comment says they "stay as stored". A save writes the whole blob, so they fall back to defaults. Note that validate.py:878 still reads the flat max_w on a roster site.
HA-down pile-up in EntityCache.get(). A failed read does not stamp the cache, so every waiter behind the lock retries. With 5 concurrent polls and HA down I saw 5 states() calls in series, which in production means up to 5 × 10 s. The next poll retries again. A failure should hold off the next attempt for CACHE_S too.
Test gap: the mutation "reversed": e in flipped → False in _solar_decode stays green. Only the identity round trip covers decode. Please add a decode check, and checks for 1 and 2.
Region wording: the preset is Fluvius Antwerpen (the other Flemish DSOs are up to +1.4 c/kWh higher, per price.py) and it comes from the Eneco card of Sept 2026. The label says only "Flanders", and nothing tells the user that Wallonia/Brussels have no preset yet. Name the DSO and state that plainly in ui_text, and cite the card and date beside REGION_PRESETS. P1 must render region_year beside the choice.
Not a finding: when a solar list is edited, pv_entity is cleared. That is TEL-12, deliberate and commented.
Follow-up, not for P0: no route checks that a request came through ingress (172.30.32.2), so add-on peers on the hassio network can reach all of them. ha/entities does not change that, but the fix belongs on a separate card for all routes.
I will push fixes for 1–5 to this branch.
## Review of UX-13 P0 (head 15627c3)
**Verified, no finding**
- `tools/ux13_validator_calls.py compare` against a `git archive` of 652097a, run in the CI image: 261 calls, 158 refused, **0 changed**. I also ran the recorded calls plus 8 malformed blobs with `notes=[]` against `notes=None`: 0 refusal diffs. `config_notes()` raised on none of them.
- Notes cannot block anything. `/config/save` and startup call the validator without `notes`, and the `/config/validate` route only adds a `notes` field.
- `GET ha/entities` exposure: its exposure is the same as every other route (8099, `ingress: true`, no `ports:` mapping, no auth of its own). It returns only entity_id, friendly name, state, unit and device_class. The Supervisor token appears only in the outgoing header and the error texts are constants. It is covered by the route-list test and the no-writer scan.
- `scenarios/ux13/`: I compared it against the live options blob. It holds no live-only value. The only overlaps are defaults (`/share/goodwe_prices.csv`, `sensor.p1_meter_active_power`) and `192.168.2.250`, which was already in test_p1/test_validate at base.
- Test changes: the route list is still exact, test_pv still excludes every decider, and test_messages now covers more text. None of them is weaker.
- Mutations: a new option and a new inverter field in config.yaml turn test_uilayout red. Dropping the `pv_invert_entities` write turns test_uimodel red.
**Findings**
1. **Blocking: editing the levies with region "own" drops `price_taxes_profile`.** `_region_encode` returns only `price_taxes_eur_kwh`. Repro: `{price_taxes_eur_kwh: 0.1234567, price_taxes_profile: '{"day":0.1}'}` → set `price_taxes_eur_kwh=0.2` → `price_taxes_profile` is missing from the output. That hits exactly the Wallonia/Brussels sites, which must choose "own". It also contradicts `uilayout.DERIVED` ("own keeps it").
2. **Blocking: editing any card of an `inverters` list drops the stored flat keys.** `soc_entity`, `batt_entity`, `batt_invert`, `max_w`, `bms_soc_floor` and `setpoint_entity` all disappear from the blob. The comment says they "stay as stored". A save writes the whole blob, so they fall back to defaults. Note that validate.py:878 still reads the flat `max_w` on a roster site.
3. **HA-down pile-up in `EntityCache.get()`.** A failed read does not stamp the cache, so every waiter behind the lock retries. With 5 concurrent polls and HA down I saw 5 `states()` calls in series, which in production means up to 5 × 10 s. The next poll retries again. A failure should hold off the next attempt for CACHE_S too.
4. **Test gap:** the mutation `"reversed": e in flipped` → `False` in `_solar_decode` stays green. Only the identity round trip covers decode. Please add a decode check, and checks for 1 and 2.
5. **Region wording:** the preset is Fluvius **Antwerpen** (the other Flemish DSOs are up to +1.4 c/kWh higher, per price.py) and it comes from the Eneco card of Sept 2026. The label says only "Flanders", and nothing tells the user that Wallonia/Brussels have no preset yet. Name the DSO and state that plainly in ui_text, and cite the card and date beside `REGION_PRESETS`. P1 must render `region_year` beside the choice.
Not a finding: when a solar list is edited, `pv_entity` is cleared. That is TEL-12, deliberate and commented.
Follow-up, not for P0: no route checks that a request came through ingress (172.30.32.2), so add-on peers on the hassio network can reach all of them. `ha/entities` does not change that, but the fix belongs on a separate card for all routes.
I will push fixes for 1–5 to this branch.
- uimodel: editing a listed inverter keeps the stored flat keys, and a
region of "own" keeps a stored price_taxes_profile (both were dropped).
- haentities: a failed read also waits CACHE_S, so polls while Home
Assistant is down do not queue one 10 s read each.
- ui_text: the Flanders preset names its DSO, and the help says
Wallonia and Brussels have no figures yet. REGION_PRESETS cites the card.
- tests: decode of a reversed solar row, both kept keys, the backoff.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes pushed in badea3c for findings 1–5 of comment 1860:
With _region_encode set to "own", a stored price_taxes_profile is kept. New check: "'own' keeps a stored levies table when the figure is edited".
_roster_encode (list) copies the stored flat keys back. setpoint_entity is still cleared only on a flat→list change (rule 9e). New check: "editing a listed inverter keeps the unread flat keys as stored".
EntityCache stamps tried_at on every attempt, so after a failure there is one read per 30 s window whatever the number of polls. New check (single loop): 5 polls down → 1 read, polls 1 s later → 0, next window → 1. It is red on 15627c3.
New decode check "a stored flip reads as a reversed row", which is red under the reversed: False mutation.
The ui_text choice is now "Flanders, Fluvius Antwerpen". The help says it is "…for the year shown, but not yet for Wallonia or Brussels, so check your bill." (one sentence, 22 words), and REGION_PRESETS cites the Eneco card of Sept 2026. P1 must render region_year beside the choice.
In the CI image: test_uimodel, test_uilayout, test_haentities, test_messages and test_selfconfig pass, and the compare tool is unchanged at 0/261. Waiting for CI.
Fixes pushed in badea3c for findings 1–5 of comment 1860:
1. With `_region_encode` set to "own", a stored `price_taxes_profile` is kept. New check: "'own' keeps a stored levies table when the figure is edited".
2. `_roster_encode` (list) copies the stored flat keys back. `setpoint_entity` is still cleared only on a flat→list change (rule 9e). New check: "editing a listed inverter keeps the unread flat keys as stored".
3. `EntityCache` stamps `tried_at` on every attempt, so after a failure there is one read per 30 s window whatever the number of polls. New check (single loop): 5 polls down → 1 read, polls 1 s later → 0, next window → 1. It is red on 15627c3.
4. New decode check "a stored flip reads as a reversed row", which is red under the `reversed: False` mutation.
5. The ui_text choice is now "Flanders, Fluvius Antwerpen". The help says it is "…for the year shown, but not yet for Wallonia or Brussels, so check your bill." (one sentence, 22 words), and `REGION_PRESETS` cites the Eneco card of Sept 2026. P1 must render `region_year` beside the choice.
In the CI image: test_uimodel, test_uilayout, test_haentities, test_messages and test_selfconfig pass, and the compare tool is unchanged at 0/261. Waiting for CI.
admin
merged commit afd6af068d into release/1.02026-09-27 23:49:32 +02:00
All points of 1860 are fixed in badea3c (see 1862). CI was green on the head (checks/unit, infra-pin/fresh). I merged with head_commit_id pinned; git ls-remote shows release/1.0 = afd6af0. Post-merge infra-pin / fresh (push) is green and no pin bump was needed.
Follow-ups: (a) an ingress-source guard for all routes (not P0); (b) Wallonia/Brussels presets once sourced figures exist; (c) P1 renders region_year beside the region choice.
All points of 1860 are fixed in badea3c (see 1862). CI was green on the head (checks/unit, infra-pin/fresh). I merged with `head_commit_id` pinned; `git ls-remote` shows release/1.0 = afd6af0. Post-merge `infra-pin / fresh (push)` is green and no pin bump was needed.
Follow-ups: (a) an ingress-source guard for all routes (not P0); (b) Wallonia/Brussels presets once sourced figures exist; (c) P1 renders `region_year` beside the region choice.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
UX-13 phase P0 (card 6ab979852f3eca02fb40f69e): the foundation for the new settings page. No visible UI change. P1 builds the page shell and Home on top of this.
What is in it
Layout table
app/uilayout.py: screen → group → part → field for all 78 options. That is 67 fields, 11 derived and 0 hidden. Each field has its control, unit, display scale, step, entity kind, show conditions, Simple/wizard placement and "applies after a restart". Ranges stay in the schema (range_of()). Words come from translations and the newapp/ui_text/en.json.Derivation
app/uimodel.py(options_to_ui/ui_to_options), covering:inverters;static_profilebuilt from a fixed or day/night price;price.py. Wallonia and Brussels need sourced figures, so those sites choose "own";arbitrage_enabled, which changes nothing yet.An unchanged derivation writes the stored values back, so the round trip is exact.
Warning tier in
validate.py(Note: warn/info). Every logged warning is also a note.config_notes()adds:/config/validatereturns these asnotes. Refusals are untouched.GET ha/entities?kind=: the picker's list (id, name, state, unit, device_class) viahass.states(). It is cached for 30 s and times out after 10 s. When HA is down it still answers 200: with the last list and "may be old", or with an empty list that says why.DECISIONS/UX-13-one-layout-table.md: why UX-02's "no option key in UI code" becomes "only in uilayout.py and uimodel.py", and how that is tested.Evidence
Round trip (options → model → JSON → options) is identical for:
652097a(scenarios/ux13/, recorded withtools/ux13_validator_calls.py);It was also run once on the live options, read-only and not committed.
Old vs new validator over those 261 calls: 158 refused, 0 differ. Passing
notes=changes no result.test_uilayout fails when an option or inverter field has no place in the table (the mutation is checked).
Tests widened on purpose:
GET /ha/entities, read-only asserted);The full suite passes locally in
goodwe-addon-checks:1(40 files).🤖 Generated with Claude Code
https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
Review of UX-13 P0 (head
15627c3)Verified, no finding
tools/ux13_validator_calls.py compareagainst agit archiveof652097a, run in the CI image: 261 calls, 158 refused, 0 changed. I also ran the recorded calls plus 8 malformed blobs withnotes=[]againstnotes=None: 0 refusal diffs.config_notes()raised on none of them./config/saveand startup call the validator withoutnotes, and the/config/validateroute only adds anotesfield.GET ha/entitiesexposure: its exposure is the same as every other route (8099,ingress: true, noports:mapping, no auth of its own). It returns only entity_id, friendly name, state, unit and device_class. The Supervisor token appears only in the outgoing header and the error texts are constants. It is covered by the route-list test and the no-writer scan.scenarios/ux13/: I compared it against the live options blob. It holds no live-only value. The only overlaps are defaults (/share/goodwe_prices.csv,sensor.p1_meter_active_power) and192.168.2.250, which was already in test_p1/test_validate at base.pv_invert_entitieswrite turns test_uimodel red.Findings
price_taxes_profile._region_encodereturns onlyprice_taxes_eur_kwh. Repro:{price_taxes_eur_kwh: 0.1234567, price_taxes_profile: '{"day":0.1}'}→ setprice_taxes_eur_kwh=0.2→price_taxes_profileis missing from the output. That hits exactly the Wallonia/Brussels sites, which must choose "own". It also contradictsuilayout.DERIVED("own keeps it").inverterslist drops the stored flat keys.soc_entity,batt_entity,batt_invert,max_w,bms_soc_floorandsetpoint_entityall disappear from the blob. The comment says they "stay as stored". A save writes the whole blob, so they fall back to defaults. Note that validate.py:878 still reads the flatmax_won a roster site.EntityCache.get(). A failed read does not stamp the cache, so every waiter behind the lock retries. With 5 concurrent polls and HA down I saw 5states()calls in series, which in production means up to 5 × 10 s. The next poll retries again. A failure should hold off the next attempt for CACHE_S too."reversed": e in flipped→Falsein_solar_decodestays green. Only the identity round trip covers decode. Please add a decode check, and checks for 1 and 2.REGION_PRESETS. P1 must renderregion_yearbeside the choice.Not a finding: when a solar list is edited,
pv_entityis cleared. That is TEL-12, deliberate and commented.Follow-up, not for P0: no route checks that a request came through ingress (172.30.32.2), so add-on peers on the hassio network can reach all of them.
ha/entitiesdoes not change that, but the fix belongs on a separate card for all routes.I will push fixes for 1–5 to this branch.
Fixes pushed in
badea3cfor findings 1–5 of comment 1860:_region_encodeset to "own", a storedprice_taxes_profileis kept. New check: "'own' keeps a stored levies table when the figure is edited"._roster_encode(list) copies the stored flat keys back.setpoint_entityis still cleared only on a flat→list change (rule 9e). New check: "editing a listed inverter keeps the unread flat keys as stored".EntityCachestampstried_aton every attempt, so after a failure there is one read per 30 s window whatever the number of polls. New check (single loop): 5 polls down → 1 read, polls 1 s later → 0, next window → 1. It is red on15627c3.reversed: Falsemutation.REGION_PRESETScites the Eneco card of Sept 2026. P1 must renderregion_yearbeside the choice.In the CI image: test_uimodel, test_uilayout, test_haentities, test_messages and test_selfconfig pass, and the compare tool is unchanged at 0/261. Waiting for CI.
All points of 1860 are fixed in
badea3c(see 1862). CI was green on the head (checks/unit, infra-pin/fresh). I merged withhead_commit_idpinned;git ls-remoteshows release/1.0 =afd6af0. Post-mergeinfra-pin / fresh (push)is green and no pin bump was needed.Follow-ups: (a) an ingress-source guard for all routes (not P0); (b) Wallonia/Brussels presets once sourced figures exist; (c) P1 renders
region_yearbeside the region choice.