UX-02: the validator's refusals, next to the field, before the save
checks / unit (pull_request) Successful in 6m17s

The Configuration tab can only render types and ranges. `validate.py`'s most
useful refusals are CROSS-FIELD - two inverters naming one `setpoint_entity`, a
`maintenance_charge_w` above `max_w`, a flat `setpoint_entity` left set beside a
declared roster - and the Supervisor's UI has no way to express a rule spanning
two keys. Today the operator meets those as exit 78 in a log, after saving and
restarting. This puts them on the screen beside the field they name, before
anything is written.

WHAT IT ADDS
  app/configui.py     the field model, built from `GET /addons/self/info`'s own
                      `schema` + `translations`. No option key, no label, no
                      rule of its own. UX-01's `Group · ` label prefix becomes a
                      real section heading, in whichever language resolved.
  app/selfconfig.py   the Supervisor conversation and the write gate.
  app/config_page.py  the page. Its longest function decides which HTML input a
                      type deserves; it contains no rule.
  app/web.py          five routes. Still the only file a reviewer has to read to
                      answer "does anything here reach the inverter".

THE FOUR THINGS THAT COULD HURT SOMEBODY, AND HOW EACH IS HELD
  1. A blob the SHIPPED validator refuses is never written - not warned about,
     not written. An invalid blob plus a restart is exit 78, and the panel is
     gone with the add-on that serves it.
  2. A blob that moved underneath is refused, not clobbered. Read hands out a
     sha256 of the canonical blob, save hands it back, and the stored blob is
     re-read IMMEDIATELY BEFORE the write. This is SAFETY-28's discipline
     applied to configuration - "a blob reaches the Supervisor only if it was
     derived from the blob stored right now" - with the honest weakening stated
     in the decision record: a second writer is DETECTED, not made
     inexpressible.
  3. Every message is the validator's own, byte for byte. test_selfconfig.py
     part 6 inserts ONE new rule into a COPY of validate.py, hashes every file
     under app/ to show validate.py is the only one that differs, and serves the
     real /config/validate route out of that copy in a subprocess. The new
     sentence arrives, filed under the key it names.
  4. Nothing on the surface commands an inverter or clears a latch. Saving does
     not restart: applying costs a control interruption - the wind-down commands
     0 W on every inverter - so that is a separate, confirmed button that says
     so first.

MEASURED, NOT CITED (tools/supervisor_selfapi_probe.py, run against
Supervisor 2026.08.0's own code; it contacts nothing)
  - the three self-paths are permitted, and `/addons/self/info` is admitted by
    the `default` role too while `options` and `restart` rest on the bypass
    ALONE. The probe's first draft claimed the role admitted none of the three
    and went red.
  - `/addons/self/options/validate` is NOT reachable from inside an add-on, so
    the only validator this panel can call is ours - which is the one with the
    cross-field rules.
  - a POST that omits a key does not preserve it: the key reverts to
    config.yaml's default. Hence whole-blob read-modify-write.
  - the Configuration tab CAN be removed, and doing so drops 0-of-71 options
    through and takes the add-on down. The card said it could not be; it stays
    by decision, which is a better reason.

DOES NOT TOUCH config.yaml, main.py or any control path. UX-01 is a dependency,
not something this supersedes: its translations are where every label,
description and section heading on this page comes from, and the Configuration
tab is still the recovery surface. DECISIONS/UX-02-two-config-surfaces.md
records which surface is authoritative (neither: the stored blob is) and what
the second surface costs.

Also corrects a stale "70 options" in three prose spots to the 71 the probe
counts - config.yaml grew a key in SAFETY-42 and the sentences did not follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

---

⚠️ REBASED ONTO release/1.0 @ 3368231, AND HISTORY DELIBERATELY FLATTENED.
The original branch carried `653874d` - an ORPHANED first attempt at PKG-07
whose commit message justified the version bump with a claim that was FALSE
(that `slew_w: 0` used to mean "no limit"; the schema was `int(50,5000)` from
the repo's first commit, so 0 was never legal). That commit's CONTENT was
superseded when release/1.0 was merged in, and the merge resolved correctly -
one 0.8.0 section, no fabricated text. But the retracted claim would have
entered release/1.0's history as a commit message with no retraction attached
to it, where PKG-07's own merged commit is the thing that retracts it.

So the branch is replayed as a single commit on top of the corrected PKG-07
rather than merged with the bad one trailing behind. The tree is byte-identical
to the reviewed branch tip; only the ancestry changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
This commit is contained in:
glenn schrooyen
2026-09-06 21:38:31 +02:00
co-authored by Claude Opus 5
parent 3368231048
commit fdaf3fb54f
12 changed files with 2903 additions and 8 deletions
+70 -1
View File
@@ -40,6 +40,69 @@ hardware holds it there.
## Configuration
### ⚠️ There are TWO places to configure this add-on (UX-02)
Since **UX-02** the ingress panel has a **Configuration** page of its own,
beside Home Assistant's *Configuration* tab. Both edit the **same** settings —
there is one options blob, and neither surface is a copy of it.
| | Ingress panel → **Configuration** | Supervisor → **Configuration** tab |
|---|---|---|
| **Use it for** | everyday editing | recovery, and when the add-on is down |
| **Sections, units, help** | real headings, units beside the boxes, the description under each field | one flat list of 71 fields, no headings, no units |
| **Tells you a setting is wrong** | **yes — before you save**, next to the field | no. It knows types and ranges only |
| **Can express a rule spanning two keys** | yes, because it runs the add-on's own validator | **no.** The schema grammar cannot say "must be at or below `max_w`" |
| **Available when the add-on is not running** | **no** — it is served *by* the add-on | **yes** |
| **Editing the inverter list** | cards you add and remove, with the per-entry labels rendered | a repeatable section whose per-entry keys render **raw** on frontend 20250925.1 |
**Which one is authoritative: neither.** The stored options blob is, and both
are editors of it. `DECISIONS/UX-02-two-config-surfaces.md` is the record, with
the measurements behind it.
The one thing you need to know in practice: **the panel refuses to save a
configuration the add-on would not start on, and the Configuration tab does
not.** The tab will happily store `maintenance_charge_w: 2500` beside
`max_w: 2000`; the add-on then exits 78 on its next start, the panel goes down
with it, and the tab is where you fix it. That asymmetry is the reason both
exist.
#### What the panel does when you press Save
1. It runs the **shipped** validator — the same `validate_startup_config()` and
`validate_unit_ranges()` the add-on runs at startup, not a copy of them — and
if anything is refused, **nothing is written** and your edits stay on screen.
2. It re-reads the stored blob and refuses if it changed while you were editing
— someone in the Configuration tab, or you on another device. It names the
keys that moved and shows both values; pressing Save again then writes yours.
3. It writes the whole blob. It does **not** restart: `POST …/options` replaces
the stored options, and a key left out would revert to the shipped default,
so the panel always sends everything it read.
#### ⚠️ Saving costs nothing. Applying costs a control interruption
Stored settings take effect when the add-on next starts. The panel shows which
stored settings are not what the add-on is currently running, and **Restart to
apply** is a separate button with its own confirmation — because the wind-down
commands **0 W on every inverter** before the process exits, and the battery
does nothing until it is back. Nothing else is changed by that restart: a
latched sign-disagreement failsafe is restored from the Store on the way back
up, not cleared.
#### Two range checks can be skipped, and the page says so
Two of the refusals compare a configured ceiling against the setpoint device's
**own** reported range, which means asking Home Assistant. If ESPHome has not
come up, that range is unknown and those two checks are skipped — on the page
and at startup, identically. The page prints a note saying so rather than
looking green for a reason you cannot see. If the device answers later and
disagrees, the add-on refuses to start then, with the same sentence.
#### On a phone
One column, big targets, sections collapsed until you open them, and the
refusals pinned where you cannot scroll past them. It is the screen for
standing in front of the inverter.
### What the Configuration tab actually shows you (UX-01)
Since **UX-01** the add-on ships `translations/en.yaml` and `translations/nl.yaml`,
@@ -52,7 +115,7 @@ whole rendered list, in both languages.
| | |
|---|---|
| **All 70 options are always shown** | The frontend keeps a field when `name in options \|\| required`, and every key is in `options:`. There is no "advanced" fold |
| **All 71 options are always shown** | The frontend keeps a field when `name in options \|\| required`, and every key is in `options:`. There is no "advanced" fold |
| **Order comes from `schema:`** | `App.schema_ui` is `UiOptions(...)(data["schema"])`, so the **schema half** of `config.yaml` decides the order the tab renders in — not the `options:` half |
| **There are no section headers** | Add-on options are a flat list: no header primitive, no per-option advanced flag, no units on the input, no sliders. Grouping is approximated by **order** plus the `Group · ` prefix in each label, and by nothing else |
| **Groups, in task order** | Hardware → Meter → Limits → Tariff → Maintenance → Safety → Advanced |
@@ -1446,6 +1509,12 @@ is, and a **Commissioning** checklist that names any problem in words. It also
carries the three buttons: start/stop control, force a maintenance cycle, and
abort one.
Since **UX-02** it has a second page, **Configuration** — see *There are TWO
places to configure this add-on*, above. ⚠️ **It adds nothing you can do to the
inverter.** The configuration page reads and writes the add-on's own settings
and can restart the add-on; the set of commands that reach the hardware is
exactly the three buttons it always had.
## Status entities
If an MQTT broker is available the add-on publishes setpoint, grid power,