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>
5.9 KiB
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_entityand noinvertersentry, 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_wabovemax_w; - no power request entity and any other refusal;
- an
inverterslist 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:
- 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. SetupOnlyrefuses a HomeAssistant that can write, andControllerrefuses one that cannot. So neither can be built on the wrong one./actionanswers 409 in setup-only mode, beforehandle_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.pypart 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 realmain()on the shipped defaults against a fake Supervisor and Home Assistant over HTTP and logs every request: the page is served,/statussays setup-only, Start and the cycle get 409, and there are 0 service calls and no MQTT request, also withauto_starton 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 thereturnafterserve()removed. - TEST-23 (PR #147 review, 2026-09-28): the add-on image on the shipped
defaults and an empty
/datamade 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'sAppOptions, and after the restart it ran in shadow: 11 of 11 writes wereset_value 0.0.
5. The wizard fixes in the same card
- F1.
/config/validateconverts 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, withsteps). - F2. A save from the page model sends
base, the model it opened. The add-on writes only the keys whose value differs frombase, 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).