MAINT-21: comments only - trim restated notes, add DECISIONS pointers
No code change: tools/maint21_same_ast.py and tools/maint20_same_code.py HEAD~ both print OK, and the -OO code objects are the same. - Trims 56 comment and docstring lines (2088 -> 2032) in app/main.py and the new files that only restated their MAIN-orchestration.md bullet, each cut to its first sentence and a pointer. What the cut comments said and the bullet did not is now in the bullet: why a SAFETY-02 latch is restored, why the SAFETY-30 site check is not armed at one inverter, the SAFETY-18 re-derive, the MAINT-05 one-config rule, the OPT-09 sizing rules, the "no plan" row, the 5 400 s age bound, and the guard being the only latch repair. - Adds the missing DECISIONS pointers on the SMA-10, SAFETY-137 and SMA-2 comments (FALLBACK_BUDGET_S, write_setpoint, _await_inflight, _release_last, the retry wait, the failed-release branch, release_zero_sent, the in-flight write, the stop_event hand-back, the SMA-2 read and driver). - DECISIONS/SAFETY-32: the stale `main.py:932-936` now names Controller.pack_floor_soc. - main.py's docstring and import block say where the Controller's parts are. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
27a867f34b
commit
902da3f723
@@ -16,7 +16,10 @@ takes the battery to zero. Never "hold the last value to be safe".
|
||||
layer 3 the optional RS485 e-stop: 0 W after 30 s of bus silence; the only
|
||||
cover for this add-on's host dying.
|
||||
|
||||
Per-section reasoning: DECISIONS/MAIN-orchestration.md."""
|
||||
Per-section reasoning: DECISIONS/MAIN-orchestration.md. The Controller keeps
|
||||
__init__, its roster views and shutdown here; its other methods are in
|
||||
ctl_inputs, ctl_writes, ctl_trade, ctl_cycle, ctl_alarms, ctl_status and
|
||||
ctl_tasks (MAINT-21), the status-row functions in rows."""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
@@ -103,6 +106,8 @@ from .watchdog import (
|
||||
)
|
||||
from . import setuponly
|
||||
from . import web
|
||||
# MAINT-21: the Controller's parts and the names they now define; every one
|
||||
# stays importable from `app.main`. DECISIONS/MAIN-orchestration.md.
|
||||
from .ctl_tasks import TasksMixin
|
||||
from .ctl_status import StatusMixin
|
||||
from .ctl_alarms import (
|
||||
@@ -190,42 +195,32 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
self.store = store
|
||||
self.mqtt = mqtt_pub
|
||||
# REQUIRED and keyword-only: a default `stop=None` made a Controller
|
||||
# that cannot stop itself (inert but looks healthy on the late-refusal
|
||||
# path in run_control()). DECISIONS/MAIN-orchestration.md.
|
||||
# that cannot stop itself. DECISIONS/MAIN-orchestration.md.
|
||||
self.stop_event = stop
|
||||
self.refused: list[str] | None = None
|
||||
# First, because the store migration below can log to it: 15's action
|
||||
# log is how an operator learns a latch was re-armed, and an
|
||||
# AttributeError here would stop the add-on booting.
|
||||
# First: the store migration below can log to it.
|
||||
# DECISIONS/MAIN-orchestration.md.
|
||||
self.events: list[str] = []
|
||||
# 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 (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.
|
||||
# DEV-02: the roster, built before anything reads a rating. The store
|
||||
# migration runs here, BEFORE the first `Inverter` reads its SAFETY-02
|
||||
# latch. 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)),
|
||||
)
|
||||
# SAFETY-30: the per-unit comparison's own pair, re-derived at two
|
||||
# inverters, not carried across (a command against one unit's own power
|
||||
# is a different quantity). DECISIONS/SAFETY-30-per-unit-sign.md;
|
||||
# watchdog.SIGN_OPTION_DEFAULTS.
|
||||
# SAFETY-30: the per-unit pair, re-derived at two inverters, not
|
||||
# carried. DECISIONS/SAFETY-30-per-unit-sign.md.
|
||||
self.unit_sign_cfg = SignWatchdogConfig(
|
||||
min_w=float(opts.get("unit_sign_disagree_w",
|
||||
SIGN_OPTION_DEFAULTS["unit_sign_disagree_w"])),
|
||||
hold_s=float(opts.get("unit_sign_disagree_s",
|
||||
SIGN_OPTION_DEFAULTS["unit_sign_disagree_s"])),
|
||||
)
|
||||
# SAFETY-30: the site comparison's own pair, used only at N>=2. The 300
|
||||
# W threshold is carried (meter noise and deadband); the window is not
|
||||
# (110 s at one inverter, 215 s at two, against a 180 s hold). See
|
||||
# watchdog.SIGN_OPTION_DEFAULTS.
|
||||
# SAFETY-30: the site pair, used only at N>=2.
|
||||
# DECISIONS/MAIN-orchestration.md.
|
||||
self.site_sign_cfg = SignWatchdogConfig(
|
||||
min_w=float(opts.get("site_sign_disagree_w",
|
||||
SIGN_OPTION_DEFAULTS["site_sign_disagree_w"])),
|
||||
@@ -239,11 +234,8 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
hold_s=float(opts.get("delivery_hold_s",
|
||||
DELIVERY_OPTION_DEFAULTS["delivery_hold_s"])),
|
||||
)
|
||||
# SAFETY-44: hand the migration the names the machines read,
|
||||
# `roster_names(opts)[0]`, never `UNIT_A` (on a declared roster the
|
||||
# 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.
|
||||
# SAFETY-44: the migration gets `roster_names(opts)[0]`, never
|
||||
# `UNIT_A`. DECISIONS/MAIN-orchestration.md.
|
||||
_roster_names = roster_names(opts)
|
||||
self.store_migration = migrate_unit_keys(
|
||||
store, _roster_names[0], _roster_names)
|
||||
@@ -252,10 +244,8 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
# restart and shadow mode (DECISIONS/SMA-10-two-idle-states.md).
|
||||
handback.set_roster(handback.units_of(self.inverters))
|
||||
uitext.set_idle(handback.overlay(handback.current()))
|
||||
# CFG-04: say which shape is in force. A declared `inverters:` roster
|
||||
# ignores `soc_entity`, `batt_entity`, `batt_invert` and `max_w`, which
|
||||
# the UI still shows; validate.py rule 9(e) refuses only the dangerous
|
||||
# one, so the rest are DISCLOSED in one action-log line at every start.
|
||||
# CFG-04: a declared roster ignores flat keys the UI still shows; say
|
||||
# so once at every start. DECISIONS/MAIN-orchestration.md.
|
||||
if roster_entries(opts):
|
||||
self.log_event(
|
||||
"roster: %d inverters from `inverters:` - %s. The flat "
|
||||
@@ -267,14 +257,11 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
|
||||
self.tuning = Tuning(
|
||||
gain=float(opts.get("gain", 0.6)),
|
||||
# The SITE bound is summed from the roster, not `opts["max_w"]`: it
|
||||
# is the integrator bound and rail of the site loop, split across
|
||||
# inverters. Identical to `max_w` at N=1; a per-unit `max_w` still
|
||||
# binds each share through its `supervised` claim.
|
||||
# The SITE bound is summed from the roster, not `opts["max_w"]`.
|
||||
# DECISIONS/MAIN-orchestration.md.
|
||||
max_w=self.site_max_w,
|
||||
# SAFETY-39: the fallback is 0, meaning "follow the rail"
|
||||
# (config.yaml and `Tuning`). It was 1000, a stale third copy.
|
||||
# `control.slew_limit_w()` is the one reader.
|
||||
# SAFETY-39: 0 means "follow the rail"; `control.slew_limit_w()`
|
||||
# is the one reader. DECISIONS/MAIN-orchestration.md.
|
||||
slew_w=float(opts.get("slew_w", 0) or 0),
|
||||
deadband_w=float(opts.get("deadband_w", 15)),
|
||||
target_grid_w=float(opts.get("target_grid_w", 0)),
|
||||
@@ -287,14 +274,9 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
integrator_max_w=(float(opts["integrator_max_w"])
|
||||
if opts.get("integrator_max_w") else None),
|
||||
)
|
||||
# CFG-03: no `opts.get(key, <literal>)` for maintenance here;
|
||||
# `MaintConfig.from_options()` reads them next to
|
||||
# `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.
|
||||
# CFG-03: `MaintConfig.from_options()` reads the options. MAINT-05:
|
||||
# one `Maintenance` per roster entry; the history migration runs first,
|
||||
# onto `self.inverters[0].name`, NOT `UNIT_A`.
|
||||
# DECISIONS/MAIN-orchestration.md.
|
||||
self.maint_migration = migrate_maint_keys(
|
||||
store, self.inverters[0].name, [inv.name for inv in self.inverters])
|
||||
@@ -306,12 +288,8 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
self.log_event("maintenance history did not move over cleanly: "
|
||||
+ self.maint_migration.explain(),
|
||||
hint=messages.hint("maint_history"))
|
||||
# MAINT-05: one config, N machines: the `maintenance_*` options
|
||||
# describe the PROCEDURE (same on every pack); the SCHEDULE and HISTORY
|
||||
# are per pack, in the store. A per-entry config block would be a
|
||||
# second shape for CFG-04 and a fifth place for `maintenance_charge_w`
|
||||
# to disagree with config.yaml; two procedures would be a roster-schema
|
||||
# card.
|
||||
# MAINT-05: one config, N machines: the options describe the
|
||||
# PROCEDURE; the SCHEDULE and HISTORY are per pack, in the store.
|
||||
_maint_cfg = MaintConfig.from_options(opts)
|
||||
_one_pack = len(self.inverters) <= 1
|
||||
self.maints = [
|
||||
@@ -329,12 +307,9 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
tz=site_tz) # MAINT-10: the start hour's zone, from amain()
|
||||
for i, inv in enumerate(self.inverters)]
|
||||
|
||||
# SAFETY-02: one watchdog per actuator, each restored from ITS OWN
|
||||
# namespaced store keys (a trip on B latches B, A keeps commanding).
|
||||
# Restored, because a latch held only in memory is cleared by the
|
||||
# restart an operator tries first. Graded on every start: a
|
||||
# disagreement with the flat value re-arms by hand and shouts (the
|
||||
# fail-safe direction is ON). DECISIONS/MAIN-orchestration.md.
|
||||
# SAFETY-02: one watchdog per actuator, restored from ITS OWN
|
||||
# namespaced keys and graded on every start.
|
||||
# DECISIONS/MAIN-orchestration.md.
|
||||
self._grade_store_migration(store)
|
||||
for inv in self.inverters:
|
||||
if inv.sign_wd.tripped:
|
||||
@@ -354,12 +329,9 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
hint=messages.hint("stop_restored",
|
||||
unit=inv.name))
|
||||
|
||||
# SAFETY-42 + SAFETY-34: a latch that does NOT act, unlike SAFETY-02's
|
||||
# and SAFETY-31's (which command 0 W). It is restored so the ALARM
|
||||
# survives (2026-09-02: an outage unseen for 6 h 25 m with the detector
|
||||
# correct); `delivery.py` argues reporting rather than acting.
|
||||
# `_delivery_announced` is per process (as `_circ_announced`): a
|
||||
# restart clears the flag and keeps the latch.
|
||||
# SAFETY-42 + SAFETY-34: a latch that does NOT act, restored so the
|
||||
# ALARM survives; `_delivery_announced` is per process, so a restart
|
||||
# clears the flag and keeps the latch. DECISIONS/MAIN-orchestration.md.
|
||||
self._delivery_announced = set()
|
||||
for inv in self.inverters:
|
||||
if inv.delivery_wd.tripped:
|
||||
@@ -376,12 +348,9 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
hint=messages.hint("not_delivering",
|
||||
unit=inv.name))
|
||||
|
||||
# SAFETY-30: the SITE half of the sign check, one per site, armed only
|
||||
# at N>=2: the SUM of the standing commands against P1. At a roster of
|
||||
# one it is constructed and never evaluated (arming it would duplicate
|
||||
# `inverters[0].sign_wd`: two latches, two banners, a false-positive
|
||||
# record spent twice); `cycle()` enforces it. Restored from the store
|
||||
# with FLAT keys (store.py). DECISIONS/SAFETY-30-per-unit-sign.md.
|
||||
# SAFETY-30: the SITE half of the sign check: armed only at N>=2 (at
|
||||
# one inverter it is constructed and never evaluated), restored with
|
||||
# FLAT keys. DECISIONS/SAFETY-30-per-unit-sign.md.
|
||||
self.site_sign_wd = SignWatchdog(
|
||||
self.site_sign_cfg,
|
||||
tripped=bool(store.data.get("site_sign_tripped", False)),
|
||||
@@ -403,15 +372,12 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
self._arm_sign_watchdogs()
|
||||
|
||||
# SAFETY-18: the frozen-but-fresh meter detector (freeze.py). NOT
|
||||
# restored from the store: it does not act, so it does not latch; a
|
||||
# restart re-derives it within one `meter_freeze_s`.
|
||||
# restored: it does not act. DECISIONS/MAIN-orchestration.md.
|
||||
self.freeze_wd = FreezeWatchdog(
|
||||
FreezeWatchdogConfig(hold_s=float(opts.get("meter_freeze_s", 300))))
|
||||
|
||||
# SAFETY-31: one per site, RESTORED (unlike SAFETY-18): a trip files a
|
||||
# P_SAFETY SET that stops every inverter, and a cleared latch would
|
||||
# restart straight into the loop it stopped. SITE-level keys, not
|
||||
# namespaced: circulating power is defined over the roster (store.py).
|
||||
# SAFETY-31: one per site, RESTORED because it acts, with SITE-level
|
||||
# keys. DECISIONS/MAIN-orchestration.md.
|
||||
self.circ_wd = CirculationWatchdog(
|
||||
CirculationConfig(
|
||||
min_w=float(opts.get("circulation_w",
|
||||
@@ -1198,6 +1164,7 @@ async def amain() -> None:
|
||||
# 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).
|
||||
# DECISIONS/SMA-2-driver-seam.md.
|
||||
_sp = str(opts.get("setpoint_entity", "") or "").strip()
|
||||
_drv = GoodWeEsphomeDriver(setpoint_entity=_sp).bind(hass)
|
||||
_dev_min = _dev_max = None
|
||||
|
||||
Reference in New Issue
Block a user