UX-15: setup-only mode, and the wizard fixes F1, F2, meter, step 1

A new install names no inverter. When that is the startup gate's only
refusal, the add-on now serves the setup wizard in setup-only mode instead
of exiting 78. It builds no controller, MQTT client or meter task, its
HomeAssistant is read-only (every service call refuses before sending),
Controller refuses a read-only HomeAssistant, and /action answers 409.
Any other refusal still exits 78. DECISIONS/UX-15-setup-only-mode.md.

Wizard fixes from the UX-13 P4 test:
- F1: /config/validate converts what it can, so a conversion error blocks
  its own step and the live checks still run; save 422 carries steps.
- F2: saves send the model the page opened (base); only keys this page
  changed are written onto the blob stored now.
- A meter entity missing from Home Assistant is red on the last step.
- Step 1 says the reading went down after a skip that followed a fall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 08:48:21 +02:00
co-authored by Claude Opus 5.5
parent 4de96752ff
commit 159d7461d8
20 changed files with 1075 additions and 71 deletions
+22 -3
View File
@@ -20,7 +20,8 @@ import json
from .price import FLEMISH_TAXES_COMPONENTS_EUR_KWH
from .static_tariff import ProfileInvalid, parse_static_profile
__all__ = ["options_to_ui", "ui_to_options", "REGION_PRESETS",
__all__ = ["options_to_ui", "ui_to_options", "ui_to_options_partial",
"REGION_PRESETS",
"arbitrage_from_batteries", "DeriveError", "COUNT", "GROWN"]
@@ -32,6 +33,7 @@ class DeriveError(ValueError):
def __init__(self, text, code="", field=""):
super().__init__(text)
self.code, self.field = code, field
self.keys = () # the option keys the failed derivation writes
# The page model's inverter count, which show conditions read, and what adding
@@ -337,14 +339,31 @@ def options_to_ui(options: dict) -> dict:
def ui_to_options(model: dict) -> dict:
"""The options blob for a page model. Raises DeriveError on a model no blob
can express, so the page says so instead of saving something else."""
out, errors = ui_to_options_partial(model)
if errors:
raise errors[0]
return out
def ui_to_options_partial(model: dict):
"""(blob, [DeriveError]): every derivation that can be made, and the
stored values (`keep`) for each one that cannot. For the checks only, so a
step can be blocked by its own field and the rest still checked (UX-15
F1). Never saved: `ui_to_options()` raises instead."""
keep = model.get("keep") or {}
out = {k: copy.deepcopy(v) for k, v in model.items()
if k not in _MODEL_ONLY and k not in _OWNED
and not k.startswith("ui.")}
errors = []
for keys, decode, encode in DERIVATIONS:
was = decode(keep)
if all(model.get(name) == value for name, value in was.items()):
out.update({k: copy.deepcopy(keep[k]) for k in keys if k in keep})
else:
continue
try:
out.update(encode(model, keep))
return out
except DeriveError as err:
err.keys = keys
errors.append(err)
out.update({k: copy.deepcopy(keep[k]) for k in keys if k in keep})
return out, errors