diff --git a/goodwe_controller/DECISIONS/MAIN-orchestration-archive.md b/goodwe_controller/DECISIONS/MAIN-orchestration-archive.md index f8693c0..18cebc5 100644 --- a/goodwe_controller/DECISIONS/MAIN-orchestration-archive.md +++ b/goodwe_controller/DECISIONS/MAIN-orchestration-archive.md @@ -2,9 +2,12 @@ Written by MAINT-20 part 2. Each section is one comment block or docstring as it stood in `app/main.py` at release/1.0 `d4c01da`, keyed by its first line number -there; the last heading holds the ones that came in by `16029d7` (keyed there). It is a record, not a design: the live rule is the one-line comment now in -the code, and the curated reasoning is in [[MAIN-orchestration]]. Comment blocks -have their `#` removed; docstrings their quotes. +there; the last heading holds the ones that came in by `16029d7` (keyed +there). It is a record, not a design: the live rule is the one-line comment now +in the code, and the curated reasoning is in [[MAIN-orchestration]]. Comment +blocks have their `#` removed; docstrings their quotes. A pointer in `main.py` +names this file only: find its section by searching here for a phrase of the +comment it ends. ### L1080 Set by amain() once the transport is built, so the status page can sho diff --git a/goodwe_controller/DECISIONS/MAIN-orchestration.md b/goodwe_controller/DECISIONS/MAIN-orchestration.md index 6567cbb..d1167d0 100644 --- a/goodwe_controller/DECISIONS/MAIN-orchestration.md +++ b/goodwe_controller/DECISIONS/MAIN-orchestration.md @@ -8,6 +8,7 @@ comments that were shortened but not rewritten here are kept word for word in [[SAFETY-30-per-unit-sign]], [[SAFETY-73-refusal-per-inverter]], [[SAFETY-121-bound-every-quarter]], [[TARIFF-10-fetching-is-not-trading]], [[TARIFF-24-modes-need-the-tariff-task]], [[OPT-34-the-night-charge-keeps-its-level]]. + ## Controller construction - **Per-write budget (DEV-02).** Each write is dispatched concurrently and bounded, so one hung `hass.call()` cannot eat a sibling's layer-1 budget (the ESP32 zeroes after about 30 s, and a fault on A could zero a healthy B). The number is derived: `heartbeat_s` max 25 s, layer-1 veto `validate.FIRMWARE_INGEST_VETO_S` 30 s, so 25 + 4 < 30. test_inverter.py grades the inequality, so raising the heartbeat ceiling goes red instead of taking an inverter to zero. Below hass.py's 10 s HTTP timeout on purpose: abandon the write and re-issue it next tick, since `last_write` moves only on an accepted write. diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index 1123ee4..b5b7460 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -162,9 +162,8 @@ TARIFF_MAX_AGE_S = 3 * TARIFF_REFRESH_S def tariff_refresh_wanted(opts: dict) -> bool: """Does this install need the background tariff refresh? (TARIFF-10, - TARIFF-24) - Yes with a price feed, trading armed, or battery modes on. It decides - whether prices are looked up, never whether energy moves. + TARIFF-24) Yes with a price feed, trading armed, or battery modes on. It + decides whether prices are looked up, never whether energy moves. DECISIONS/TARIFF-10-fetching-is-not-trading.md and DECISIONS/TARIFF-24-modes-need-the-tariff-task.md.""" return (provider_configured(opts) or arbitrage.enabled(opts) @@ -4666,7 +4665,9 @@ class Controller: "lower: %s.", messages.reason_code(g.reason)) _LOG.debug("Evidence: %s.", g.reason) # A fixed notification_id, and a DIFFERENT one from every other alarm - # here - check_meter_frozen() gives the reasoning. + # here - check_meter_frozen() gives the reasoning. ⚠️ SAFETY-25's + # sentence (the reserve binds the peak guard only) is in + # messages.peak_reserve() on every branch, unreadable SoC included. # (DECISIONS/MAIN-orchestration-archive.md) await self.hass.call("persistent_notification", "create", messages.peak_reserve( @@ -5183,8 +5184,9 @@ class Controller: elif rail_unreachable(self.site_max_w, dev_min=self.site_dev_bound("dev_min"), dev_max=self.site_dev_bound("dev_max")): - # SAFETY-20, and THE SECOND REASON TO USE THIS ROW RATHER THAN - # A NEW MECHANISM. (DECISIONS/MAIN-orchestration-archive.md) + # SAFETY-20: the same row as the transport case above (the + # check cannot be evaluated here); only the missing term + # differs. (DECISIONS/MAIN-orchestration-archive.md) _no_rating = (not self.site_max_w or not is_commandable(self.site_max_w)) out.append({"ok": False, "warn": True, "id": "freeze_check", @@ -5476,8 +5478,9 @@ class Controller: inv.name, _dheld, inv.delivery_wd.cfg.hold_s)}) else: - # AC 5, and the same honesty `circulating power: n/a` uses one - # block up. (DECISIONS/MAIN-orchestration-archive.md) + # AC 5: at one inverter this check has nothing to find, so it is + # not called "armed" (as `circulating power: n/a` above). + # (DECISIONS/MAIN-orchestration-archive.md) out.append({"ok": True, "warn": False, "id": "delivery", "text": messages.row_one_inverter("Delivery check")}) @@ -5598,7 +5601,8 @@ class Controller: level, banner = "bad", messages.banner_direction( len(self.inverters) > 1) elif bad: - # ⚠️ `banner` IF THE ROW OFFERS ONE, `text` OTHERWISE. + # ⚠️ `banner` IF THE ROW OFFERS ONE, `text` OTHERWISE: a row may + # say more than fits the headline (SAFETY-25's floor row). # (DECISIONS/MAIN-orchestration-archive.md) level, banner = "bad", messages.banner_not_ready(bad[0]) # SAFETY-112: nothing to do but wait, so Home says that instead