From a5fec377ad241091c4f41cf9d756229da645f589 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Sun, 8 Nov 2026 18:06:00 +0100 Subject: [PATCH] MAINT-20 part 2 r2: app/main.py comments, lines 1-500 (tariff_refresh_wanted .. __init__) Co-Authored-By: Claude --- goodwe_controller/app/main.py | 68 +++++++++++++++-------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index db5dcd1..ebb8b42 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -113,14 +113,13 @@ _LOG = logging.getLogger("goodwe") # function CFG-04 changes. __all__ = ["UNIT_A", "Controller", "amain", "main"] -# DEV-02: the per-write budget. Every write to every inverter is dispatched -# concurrently, each bounded by this, so one hung `hass.call()` cannot eat a -# sibling's layer-1 budget. -# DERIVED, NOT CHOSEN: `heartbeat_s` max is 25 s and layer 1's veto is -# `validate.FIRMWARE_INGEST_VETO_S` (30 s), so 25 + 4 < 30. test_inverter.py -# grades the inequality, not the literal. It is below hass.py's 10 s HTTP -# timeout on purpose: abandon and re-issue next tick (`last_write` moves only -# on an ACCEPTED write). DECISIONS/MAIN-orchestration.md. +# DEV-02: the per-write budget: every write is dispatched concurrently, each +# bounded by this, so one hung `hass.call()` cannot eat a sibling's layer-1 +# budget. DERIVED: `heartbeat_s` max 25 s + 4 < +# `validate.FIRMWARE_INGEST_VETO_S` (30 s), graded by test_inverter.py; below +# hass.py's 10 s HTTP timeout on purpose (abandon, re-issue next tick; +# `last_write` moves only on an ACCEPTED write). +# DECISIONS/MAIN-orchestration.md. WRITE_BUDGET_S = 4.0 # SMA-10: the bound for handing a unit back (`driver.release()`); units are @@ -300,14 +299,10 @@ def why_key(winner: str, claims) -> str: def tariff_plan_row(plan, tick_words: str, *, age_s, horizon_h: float, provider: bool, max_age_s: float) -> str: """The *Tariff* status row: the 30 s tick's sentence AND the provider plan - (TARIFF-11). - Two sources, two clocks, and the row says so: `tick_words` is regenerated - on this I/O-free tick; `plan` is the refresh's resolution, stamped with its - own age from `tariff_at` (not moved on a kept plan, TARIFF-12), up to - TARIFF_MAX_AGE_S (5400 s) before `fresh_tariff_plan()` retires it. `age_s` - is the caller's measurement: a clock read here would disagree with the one - that judged freshness. "No plan" is said aloud on a site that configured a - provider (a soak read 'no static profile' at all 802 samples). + (TARIFF-11): two sources, two clocks, and the row says so. The plan is + stamped with its own age from `tariff_at` (not moved on a kept plan, + TARIFF-12), up to TARIFF_MAX_AGE_S (5400 s); `age_s` is the caller's + measurement. "No plan" is said aloud on a site that configured a provider. DECISIONS/MAIN-orchestration.md.""" # UX-08: plain words, every figure kept. The tick's sentence is "now"; the # plan carries its own age. @@ -373,14 +368,13 @@ class Controller: # UX-08: the last event in plain words, for the page. See log_event(). self.hint = "" - # DEV-02: the roster, built before anything reads a rating. Every - # per-device value lives on these objects and nowhere else - # (app/inverter.py); a `Tuning` and an integrator are deliberately NOT - # per device (6.2: one integrator on P1 above the split). The store - # migration runs here, BEFORE the first `Inverter` reads its SAFETY-02 - # latch: else an existing tripped install comes up ARMED with a green - # banner. SAFETY-02's pair is the site comparison's pair (SAFETY-30), - # identical at N=1. DECISIONS/MAIN-orchestration.md. + # DEV-02: the roster, built before anything reads a rating; every + # per-device value lives on these objects (app/inverter.py), but a + # `Tuning` and the integrator are deliberately NOT per device (6.2: one + # integrator on P1 above the split). The store migration runs here, + # BEFORE the first `Inverter` reads its SAFETY-02 latch, else a tripped + # install comes up ARMED with a green banner. + # DECISIONS/MAIN-orchestration.md. self.sign_cfg = sign_cfg = SignWatchdogConfig( min_w=float(opts.get("sign_disagree_w", 300)), hold_s=float(opts.get("sign_disagree_s", 180)), @@ -412,14 +406,11 @@ class Controller: hold_s=float(opts.get("delivery_hold_s", DELIVERY_OPTION_DEFAULTS["delivery_hold_s"])), ) - # SAFETY-44: hand the migration the names the machines read: + # SAFETY-44: hand the migration the names the machines read, # `roster_names(opts)[0]`, never `UNIT_A` (on a declared roster the - # flat latch landed under `unit/inverter-a/`, on no roster, and stamped - # the marker). `roster_names()` returns `[UNIT_A]` on the flat shape, - # so today's upgrade path is byte-identical. `misdirected` is - # unreachable from THIS call site; the `roster` argument stays for new - # callers and is graded in test_maintenance.py. - # DECISIONS/MAIN-orchestration.md. + # latch landed under `unit/inverter-a/`, on no roster, and stamped the + # marker). `roster_names()` is `[UNIT_A]` on the flat shape, so today's + # path is byte-identical. DECISIONS/MAIN-orchestration.md. _roster_names = roster_names(opts) self.store_migration = migrate_unit_keys( store, _roster_names[0], _roster_names) @@ -465,15 +456,12 @@ class Controller: ) # CFG-03: no `opts.get(key, )` for maintenance here; # `MaintConfig.from_options()` reads them next to - # `maintenance.MAINT_OPTION_DEFAULTS` (two literals once disagreed with - # config.yaml). A new key goes in config.yaml `options:`/`schema:` and - # that dict only. - # MAINT-05: one `Maintenance` per roster entry. The history migration - # runs first (else a pack believes it was never recalibrated), and - # targets `self.inverters[0].name`, NOT `UNIT_A`: the flat keys - # belonged to the first roster entry. The roster is passed, not a - # count, so `migrate_maint_keys()` grades the namespace it wrote - # (test_maintenance.py sections 9 and 11 use `["garage", "shed"]`). + # `maintenance.MAINT_OPTION_DEFAULTS`; a new key goes in config.yaml + # `options:`/`schema:` and that dict only. MAINT-05: one `Maintenance` + # per roster entry; the history migration runs first (else a pack + # believes it was never recalibrated) and targets + # `self.inverters[0].name`, NOT `UNIT_A`; the roster is passed so + # `migrate_maint_keys()` grades the namespace it wrote. # DECISIONS/MAIN-orchestration.md. self.maint_migration = migrate_maint_keys( store, self.inverters[0].name, [inv.name for inv in self.inverters])