UX-02: the validator's refusals, next to the field, before the save
checks / unit (pull_request) Successful in 6m17s
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:
co-authored by
Claude Opus 5
parent
3368231048
commit
fdaf3fb54f
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user