"""UX-15: setup-only mode. A new install serves the setup wizard and commands nothing. The rule and the argument are in DECISIONS/UX-15-setup-only-mode.md. `applies()` says when: the startup gate's only refusal is that no inverter is named. `serve()` then runs the ingress page on a `SetupOnly` stand-in for the controller. Nothing that can command is built: no Controller, no inverter, no MQTT, no meter task. Its HomeAssistant is read-only, so a service call refuses before anything is sent (hass.py), and `/action` refuses too (web.py). """ import asyncio import contextlib import logging import signal from datetime import datetime, timezone from . import messages, web from .inverter import roster_entries from .validate import actuator_writers, no_power_request_problem _LOG = logging.getLogger("goodwe.setup") def applies(opts: dict, problems) -> bool: """True when the options name no inverter at all and that is the gate's only refusal. Anything else refused keeps the exit 78 (CFG-01).""" return (not actuator_writers(opts) and not roster_entries(opts) and list(problems) == [no_power_request_problem()]) class SetupOnly: """What web.build_app() needs from a controller, and nothing that writes. It holds no inverter and no publisher. `hass` must be read-only.""" setup_only = True auto = False # control is never started here grid = None # no meter is read here inverters = () def __init__(self, opts: dict, hass, store): if getattr(hass, "read_only", False) is not True: raise ValueError("setup-only mode needs a read-only HomeAssistant") self.o, self.hass, self.store = opts, hass, store self.events: list[str] = [] self.hint = "" def log_event(self, msg: str, hint: str | None = None) -> None: stamp = datetime.now(timezone.utc).strftime("%H:%M:%S") self.events.insert(0, f"{stamp} {msg}") del self.events[40:] if hint is not None: self.hint = f"{datetime.now().astimezone():%H:%M} {hint}" def status_payload(self) -> dict: return {"setup_only": True, "mode": "setup_only", "banner": messages.banner_setup_only(), "level": "warn", "rows": [["Control", "not started, in setup"]], "checks": [{"ok": False, "warn": True, "text": messages.row_setup_only()}], "hint": self.hint, "events": list(self.events)} async def handle_action(self, what): # web.py answers 409 before it gets here. Nothing is done either way. _LOG.warning("The page asked for an action in setup, so nothing " "was done.") return None async def shutdown(self) -> None: """Nothing to wind down: nothing was ever commanded.""" async def serve(opts: dict, hass, store, *, port: int) -> None: """Serve the page in setup-only mode until the Supervisor stops us.""" controller = SetupOnly(opts, hass, store) _LOG.warning("No inverter is named yet, so the add-on is in setup and " "controls nothing. Open its page to finish setup.") controller.log_event("setup-only mode: no inverter named, nothing is " "commanded", hint=messages.hint("setup_only")) runner = await web.start(controller, port=port) stop = asyncio.Event() loop = asyncio.get_running_loop() for sig in (signal.SIGTERM, signal.SIGINT): with contextlib.suppress(NotImplementedError): loop.add_signal_handler(sig, stop.set) try: await stop.wait() finally: await runner.cleanup() _LOG.info("stopped")