diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index 85c533c..1123ee4 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -6021,7 +6021,8 @@ async def amain() -> None: hass = HomeAssistant(session) # Startup self-check: prove we can actually reach the Core API before - # anything tries to control an inverter with it. + # anything tries to control an inverter with it. A 401 here is a + # permissions/token problem, not a configuration mistake. # (DECISIONS/MAIN-orchestration-archive.md) import os as _os _tok = _os.environ.get("SUPERVISOR_TOKEN", "") @@ -6037,13 +6038,14 @@ async def amain() -> None: _LOG.error("core api probe failed: %s", _e) # UX-09: the Details link in every notification needs this add-on's - # slug and Home Assistant's version (see messages.docs_url()). - # (DECISIONS/MAIN-orchestration-archive.md) + # slug and Home Assistant's version (see messages.docs_url()); both are + # best effort. (DECISIONS/MAIN-orchestration-archive.md) messages.set_site(**await docs_link_facts(session, _tok)) # ⚠️ THE STARTUP GATE (CFG-01). Nothing below this line may run on a - # configuration validate.py rejects - specs §1.5: a config the system - # cannot honour is refused at startup, not survived at runtime. + # configuration validate.py rejects (specs §1.5). It sits HERE, before + # the broker, web server and any task; (None, None) from an ESPHome not + # yet up is not a refusal (run_control() re-checks). # (DECISIONS/MAIN-orchestration-archive.md) # SMA-2: the flat options are the GoodWe driver's (no Controller yet). _sp = str(opts.get("setpoint_entity", "") or "").strip() @@ -6055,8 +6057,8 @@ async def amain() -> None: _problems = validate_startup_config(opts, _dev_min, _dev_max) # UX-15: a new install names no inverter. When that is the ONLY refusal # it serves the setup wizard in setup-only mode, which builds nothing - # that commands and returns when stopped. - # (DECISIONS/MAIN-orchestration-archive.md) + # that commands and returns when stopped; any other refusal still exits + # 78. DECISIONS/UX-15-setup-only-mode.md if _problems and setuponly.applies(opts, _problems): await setuponly.serve( opts, HomeAssistant(session, read_only=True), store, @@ -6066,9 +6068,9 @@ async def amain() -> None: refuse(_problems) raise ConfigRefused(_problems) - # ⚠️ Status publishing must NEVER be able to stop the controller. A - # broken broker, a missing library, an API change in paho - all of it - # is observability, and the battery does not care. + # ⚠️ Status publishing must NEVER be able to stop the controller: + # caught broadly and on purpose (a paho 1.x/2.x change once crashed the + # add-on and took the control loop with it). # (DECISIONS/MAIN-orchestration-archive.md) broker = None try: @@ -6084,9 +6086,12 @@ async def amain() -> None: omit=() if is_enabled(opts) else ("p1_age",), # TEL-11. ⚠️ THE SHIPPED PREDICATE, NOT A SECOND READING OF THE # OPTION - `Controller.publish()` gates the row on this same - # call. (DECISIONS/MAIN-orchestration-archive.md) + # call; an install with no PV roster announces exactly what it + # did before. (DECISIONS/MAIN-orchestration-archive.md) include=optional_keys(opts), - # TEL-07. ⚠️ THE NAMES, NOT A ROSTER. + # TEL-07. ⚠️ THE NAMES, NOT A ROSTER: `build_roster()` here + # would build a SECOND set of `Inverter`s off the Store; + # `roster_names()` is the naming rule on its own. # (DECISIONS/MAIN-orchestration-archive.md) units=roster_names(opts), modes=_modes, @@ -6122,10 +6127,11 @@ async def amain() -> None: control_task_done, controller=controller, stop=stop, died=died)) tasks = [control] - # OPT-04. - # STARTED FOR A PROVIDER, FOR ARMED TRADING OR FOR BATTERY MODES: see - # `tariff_refresh_wanted()` above and DECISIONS/TARIFF-10 and TARIFF-24 - # for why each term is there. + # OPT-04. The tariff refresh runs as its OWN task (`resolve_tariff()` + # may fetch over HTTPS, and the tick feeds the ESP32 heartbeat). + # Started for a provider, armed trading or battery modes + # (`tariff_refresh_wanted()`). ⚠️ IT DOES NOT ARM ANYTHING: + # `arbitrage.enabled()` in cycle() is the only gate on trading. # (DECISIONS/MAIN-orchestration-archive.md) if tariff_refresh_wanted(opts): tasks.append(asyncio.create_task(controller.run_tariff_refresh())) @@ -6165,8 +6171,9 @@ def main() -> None: except KeyboardInterrupt: pass except ConfigRefused: - # ⚠️ No traceback, on purpose. - # (DECISIONS/MAIN-orchestration-archive.md) + # ⚠️ No traceback, on purpose: the banner refuse() printed says + # everything. 78 is sysexits EX_CONFIG, so a script can tell a bad + # config from a bug. (DECISIONS/MAIN-orchestration-archive.md) raise SystemExit(78) except ControlTaskDied: # No second traceback: control_task_done() logged it at the death.