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:
@@ -808,6 +808,14 @@ class ConfigRefused(Exception):
|
||||
super().__init__(problems[0] if problems else "configuration refused")
|
||||
|
||||
|
||||
def no_power_request_problem() -> str:
|
||||
"""Rule 1's refusal: no inverter to control. UX-15's setup-only mode
|
||||
starts when this is the only refusal (app/setuponly.py)."""
|
||||
return (f"{S('setpoint_entity')} is empty, so the add-on cannot control "
|
||||
"the inverter. Set it to the ESPHome number entity for the "
|
||||
"setpoint.")
|
||||
|
||||
|
||||
def validate_startup_config(opts: dict, dev_min=None, dev_max=None,
|
||||
notes: list | None = None) -> list[str]:
|
||||
"""Return the list of reasons this configuration must not be started.
|
||||
@@ -828,9 +836,7 @@ def validate_startup_config(opts: dict, dev_min=None, dev_max=None,
|
||||
# --- 1. an actuator to write to, and AT MOST ONE WRITER PER ACTUATOR -----
|
||||
writers = actuator_writers(opts)
|
||||
if not writers:
|
||||
problems.append(
|
||||
f"{S('setpoint_entity')} is empty, so the add-on cannot control the "
|
||||
"inverter. Set it to the ESPHome number entity for the setpoint.")
|
||||
problems.append(no_power_request_problem())
|
||||
# SAFETY-28. The §1.5 non-goal, graded on the writers rather than on the
|
||||
# count of option keys. Read `actuator_writers()` for why the old check went
|
||||
# green on the very config this one is here to refuse.
|
||||
@@ -1704,6 +1710,14 @@ WATT_ENTITY_KEYS = (
|
||||
"p1_phase_export_entities", "batt_entity", "peak_forecast_entity")
|
||||
|
||||
|
||||
# The meter entities each `meter_source` reads through Home Assistant.
|
||||
METER_ENTITY_KEYS = {
|
||||
"off": ("meter_entity",),
|
||||
SOURCE_HA_SIGNED: ("p1_net_entity", "p1_phase_net_entities"),
|
||||
SOURCE_HA: ("p1_import_entity", "p1_export_entity",
|
||||
"p1_phase_import_entities", "p1_phase_export_entities")}
|
||||
|
||||
|
||||
def _entities_read(opts: dict) -> list[tuple[str, str, str]]:
|
||||
"""(key, inverter name or "", entity id) for each entity the add-on reads
|
||||
with these options. Keys the chosen meter source or roster shape ignores
|
||||
@@ -1716,12 +1730,7 @@ def _entities_read(opts: dict) -> list[tuple[str, str, str]]:
|
||||
out.append((key, where, e.strip()))
|
||||
|
||||
source = str(opts.get("meter_source") or "off").strip()
|
||||
by_source = {"off": ("meter_entity",),
|
||||
SOURCE_HA_SIGNED: ("p1_net_entity", "p1_phase_net_entities"),
|
||||
SOURCE_HA: ("p1_import_entity", "p1_export_entity",
|
||||
"p1_phase_import_entities",
|
||||
"p1_phase_export_entities")}
|
||||
for key in by_source.get(source, ()):
|
||||
for key in METER_ENTITY_KEYS.get(source, ()):
|
||||
add(key, opts.get(key))
|
||||
entries = roster_entries(opts)
|
||||
if not entries:
|
||||
@@ -1844,7 +1853,7 @@ def setup_checks(opts: dict, *, entities: dict | None, running=None,
|
||||
entity exists in Home Assistant (a write to a missing one is answered 200,
|
||||
SAFETY-77, so only the entity list can tell), the battery levels read, and
|
||||
prices are known. Tier "ok", "info", "warn", or "bad" for a missing power
|
||||
request entity, which the wizard will not save. Reads nothing itself:
|
||||
request or meter entity, which the wizard will not save. Reads nothing itself:
|
||||
`entities` is Home Assistant's list by id (None when it did not answer),
|
||||
`running` the options this process started with, `meter_now_w` its meter
|
||||
reading and `prices_known` how many of today's hours have a price.
|
||||
@@ -1855,7 +1864,11 @@ def setup_checks(opts: dict, *, entities: dict | None, running=None,
|
||||
_advise(notes, "bad", ("setpoint_entity",), "Home Assistant is not "
|
||||
"answering, so press Check again once it is back.")
|
||||
else:
|
||||
_entity_notes(notes, opts, entities, bad=("setpoint_entity",))
|
||||
# A missing meter entity is red too (UX-15): with the kettle test
|
||||
# skipped, nothing else on the way to the save would catch it.
|
||||
meter_keys = {k for keys in METER_ENTITY_KEYS.values() for k in keys}
|
||||
_entity_notes(notes, opts, entities,
|
||||
bad=("setpoint_entity",) + tuple(sorted(meter_keys)))
|
||||
meter = _meter_check(notes, opts, entities, running, meter_now_w)
|
||||
batts = [_card_checks(notes, who, entry, entities)
|
||||
for who, entry in _cards(opts)]
|
||||
|
||||
Reference in New Issue
Block a user