It said the PR had TEST-23 evidence, but the PR says TEST-23 was not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.9 KiB
Markdown
115 lines
5.9 KiB
Markdown
# UX-15 — setup-only mode: a new install serves the wizard
|
|
|
|
**Status: DECIDED 2026-09-28 (owner decision; UX-15 dev).** Card
|
|
6aba0053d27a32cb3f210dcc. Changes the CFG-01 startup gate and closes the open
|
|
question in [[UX-13-setup-wizard]] §2. Amends [[UX-02-two-config-surfaces]] §4.
|
|
|
|
## 1. The rule
|
|
|
|
`config.yaml` ships `setpoint_entity: ""`, so a new install's options are
|
|
refused by rule 1 of `validate_startup_config()`: no inverter to control.
|
|
Until now that was exit 78 before any page was served, and the installer had
|
|
to fill in the entity on the Configuration tab before the wizard could open.
|
|
|
|
The add-on now starts in **setup-only mode** when, and only when
|
|
(`setuponly.applies()`):
|
|
|
|
- the options name no inverter at all: no flat `setpoint_entity` and no
|
|
`inverters` entry, and
|
|
- rule 1's refusal (`no_power_request_problem()`) is the gate's **only**
|
|
refusal.
|
|
|
|
In setup-only mode it serves the page, says it is in setup (banner, `/status`
|
|
`setup_only: true`, a WARNING log line) and opens the wizard when the page is
|
|
opened with no route (`setup_needed` is `no_power_request`). After the
|
|
wizard's save and restart the normal gate applies to the saved options.
|
|
|
|
## 2. What still exits 78, and why
|
|
|
|
Everything else the gate refuses, including:
|
|
|
|
- a complete configuration that is refused, e.g. `maintenance_charge_w` above
|
|
`max_w`;
|
|
- no power request entity **and** any other refusal;
|
|
- an `inverters` list whose entry has a blank power request entity.
|
|
|
|
These are configurations somebody edited. The Configuration tab stored them,
|
|
the log's refusal banner names each problem, and the tab is where they are
|
|
fixed (UX-02 §4). Serving the wizard there would put a six-step setup in
|
|
front of someone who needs to change one field. A narrow rule also keeps the
|
|
setup-only decision to one comparison against one known sentence, so a new
|
|
validator rule cannot widen it by accident: any other refusal fails the
|
|
equality and exits 78.
|
|
|
|
The cost: an installer who first sets, say, a DSMR meter with no entities on
|
|
the Configuration tab and leaves the power request entity empty gets exit 78,
|
|
not the wizard. The log then names both fields.
|
|
|
|
## 3. Why it cannot command
|
|
|
|
Setup-only mode builds nothing that commands. `amain()` branches to
|
|
`setuponly.serve()` before the MQTT broker, the controller, the meter task and
|
|
the tariff task are built, and it returns from there when the Supervisor
|
|
stops it. What serves the page is `SetupOnly`, which holds no inverter.
|
|
|
|
Three checks make it structural rather than a convention:
|
|
|
|
1. **The HomeAssistant is read-only** (`HomeAssistant(read_only=True)`).
|
|
`call()` refuses before it sends anything, and every service call goes
|
|
through it: `number.set_value` (`set_number()`), notifications, all of
|
|
them. The flag is fixed at construction, and there is no setter.
|
|
2. **`SetupOnly` refuses a HomeAssistant that can write, and `Controller`
|
|
refuses one that cannot.** So neither can be built on the wrong one.
|
|
3. **`/action` answers 409** in setup-only mode, before
|
|
`handle_action()`. Start, Stop and the battery cycle do nothing.
|
|
|
|
No MQTT client is created, so nothing is published. The page's other routes
|
|
already read only, or write the add-on's own options (UX-02 §6).
|
|
|
|
Setup-only mode writes no Store file. The wizard's save writes `setup_done`,
|
|
as in any mode. So a new install that is set up on the Configuration tab
|
|
instead still has a fresh Store on its first normal boot, and the first-run
|
|
rule (UX-13 §2) sends it to the wizard as before.
|
|
|
|
## 4. How it is held
|
|
|
|
- `test_setup_only.py` part 1: when it applies, including the three exit-78
|
|
cases above. Part 2: the read-only write path refuses with nothing sent,
|
|
and the two constructors refuse. Part 3 boots the real `main()` on the
|
|
shipped defaults against a fake Supervisor and Home Assistant over HTTP and
|
|
logs every request: the page is served, `/status` says setup-only, Start
|
|
and the cycle get 409, and there are 0 service calls and no MQTT request,
|
|
also with `auto_start` on and a Store that says control was started. The
|
|
wizard's save is then booted: it passes the normal gate, does not open the
|
|
wizard again, and every write is 0 W (shadow). Part 4: the refused
|
|
configurations exit 78 and serve nothing.
|
|
- Mutations, each red: `applies()` always false or always true, the
|
|
read-only refusal removed, the Controller's check removed, `amain()` giving
|
|
setup-only mode a writable HomeAssistant, `/action`'s 409 removed, and the
|
|
`return` after `serve()` removed.
|
|
- TEST-23 (PR #147 review, 2026-09-28): the add-on image on the shipped
|
|
defaults and an empty `/data` made 0 service calls and no MQTT request in
|
|
3.5 minutes of setup-only mode, counted in both the stand-in and the rig
|
|
shim logs. The wizard save then passed Supervisor 2026.08.0's `AppOptions`,
|
|
and after the restart it ran in shadow: 11 of 11 writes were
|
|
`set_value 0.0`.
|
|
|
|
## 5. The wizard fixes in the same card
|
|
|
|
- **F1.** `/config/validate` converts what it can of a page model
|
|
(`uimodel.ui_to_options_partial()`), so a conversion error blocks its own
|
|
step beside its field and the live checks still run on the rest. A refusal
|
|
about a key the failed conversion writes is left out, because it would
|
|
judge the stored value. The save still refuses such a model (422, with
|
|
`steps`).
|
|
- **F2.** A save from the page model sends `base`, the model it opened. The
|
|
add-on writes only the keys whose value differs from `base`, onto the blob
|
|
stored now (`uipage.only_edits()`). So "save again to keep yours" after a
|
|
409 keeps the other surface's changes. A key both surfaces changed takes
|
|
this page's value, which is what the second press after the banner means.
|
|
- **Meter entity.** On the last step a meter entity Home Assistant does not
|
|
have is red, like a missing power request entity, so a skipped kettle test
|
|
cannot let it through. In the Installer view it stays a warning.
|
|
- **Step 1 after a skip.** When the kettle test saw the reading fall and was
|
|
then skipped, step 1 says the reading went down. The skip stays (UX-13 §3).
|