diff --git a/goodwe_controller/app/main.py b/goodwe_controller/app/main.py index 564da33..994f6b7 100644 --- a/goodwe_controller/app/main.py +++ b/goodwe_controller/app/main.py @@ -3023,25 +3023,15 @@ class Controller: def write_refusal(self, value: float, inv=None) -> str | None: """Why `value` may NOT be written to `inv` right now, or None if it - may. - - SAFETY-28: a number reaches the wire only if THE SINGLE ALLOCATOR - placed it, on this tick, for THIS unit (`Allocation.authorises()`). - specs 1.5 makes two writers on one actuator a non-goal; - `len(validate.ACTUATOR_WRITE_KEYS) == 1` is a statement about the - options schema, not the writes. A second writer must file a Claim and - go through `resolve()`. - 0 W is ALWAYS authorised and the test for zero is first and reads - nothing: the failsafe writes run while the allocator has REFUSED, so a - gate asking its permission to command zero would deadlock the failsafe - behind the component whose failure it answers; and at S = 0 broadcast - and allocation coincide (no over-command). It tests `value == 0.0`, not - a `failsafe=True` flag (a flag is an assertion by the caller, which is - what a second writer would pass). - DEV-02: the unit is an ARGUMENT: the record holds one share per unit, - and the first inverter's name at a roster of two would authorise A's - share on B's wire. The default is for N=1 callers and REFUSES at N>=2 - (`_only()`). DECISIONS/MAIN-orchestration.md.""" + may. SAFETY-28: a number reaches the wire only if THE SINGLE ALLOCATOR + placed it, on this tick, for THIS unit (`Allocation.authorises()`); a + second writer must file a Claim. 0 W is ALWAYS authorised and tested + first, reading nothing: the failsafe writes run while the allocator has + REFUSED, so a gate asking permission to command zero would deadlock the + failsafe. It tests `value == 0.0`, not a flag the caller sets. DEV-02: + the unit is an ARGUMENT (the record holds one share per unit); the + default is for N=1 callers and REFUSES at N>=2. + DECISIONS/MAIN-orchestration.md.""" if value == 0.0: return None inv = self._only() if inv is None else inv @@ -3311,8 +3301,10 @@ class Controller: if n] def _provider_gate(self, now: datetime) -> None: - """PRICE-01: `cheap`, the gate and the gap from the standing plan. - (DECISIONS/MAIN-orchestration-archive.md)""" + """PRICE-01: `cheap`, the gate and the gap from the standing plan. No + price for now is a closed gate, so the charge waits rather than run + unpriced; the gap flag stays False until the first refresh has + finished. (DECISIONS/MAIN-orchestration-archive.md)""" plan = self.fresh_tariff_plan(now) published = None if plan is None else plan.published if published is not None: @@ -3349,14 +3341,17 @@ class Controller: def _charge_gate_open(self, inv) -> bool: """MAINT-13: may this pack charge now, for the maintenance timeout? - (DECISIONS/MAIN-orchestration-archive.md)""" + Closed only when a price gate exists, it is shut, and the pack is not + really taking charge. (DECISIONS/MAIN-orchestration-archive.md)""" if not self._price_gated or self.cheap: return True batt = inv.batt return batt is not None and float(batt) <= -MAINT_CHARGING_W async def cycle(self) -> None: - """Gather claims, resolve precedence, write the winner. + """Gather claims, resolve precedence, write the winner. Nothing here + decides who wins (arbiter.py does, by one rule); this method only + states what each strategy wants. (DECISIONS/MAIN-orchestration-archive.md)""" now = datetime.now(timezone.utc) # BEFORE the first `return` of this method on purpose (TARIFF-11): it