MAINT-20 part 2 r2: app/main.py comments, lines 5001-5500 (shutdown .. checks)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
31d13ee8bb
commit
b05ad2b273
+102
-46
@@ -5002,41 +5002,60 @@ class Controller:
|
||||
"soc": self.pack_floor_soc,
|
||||
"phase": self.maint.phase,
|
||||
# ⚠️ SAFETY-02's alarm surface, and it is deliberately an existing
|
||||
# one. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# one: "failsafe" is additive to a sensor already on every
|
||||
# dashboard, and a P_SAFETY stop that did not reach it would be a
|
||||
# site at 0 W reporting "running".
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
"status": ("failsafe" if any(i.sign_wd.tripped
|
||||
for i in self.inverters)
|
||||
or (self.site_sign_wd.tripped
|
||||
and len(self.inverters) > 1)
|
||||
or self.circ_wd.tripped
|
||||
# SAFETY-10: "shadow" is its own value and not a flavour
|
||||
# of "stopped".
|
||||
# of "stopped". ⚠️ BELOW `failsafe`: a latch that is up
|
||||
# is up, and shadow does not clear it.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
else "shadow" if self.shadow
|
||||
else "running" if self.auto else "stopped"),
|
||||
# SAFETY-31. ⚠️ MEASURED, not the allocation record - see
|
||||
# `site_circulating_w`. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# `site_circulating_w`. `publish()` drops None values, so a missing
|
||||
# per-unit reading leaves the last figure standing rather than
|
||||
# publishing a zero that would read as health.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
"circulation": (None if self.site_circulating_w is None
|
||||
else round(self.site_circulating_w)),
|
||||
# TEL-10. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# TEL-10. The loop's own three, written from the fields `cycle()`
|
||||
# recorded and not re-derived (the page and the publisher run on
|
||||
# their own tasks, and `self.target` has moved on).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
"loop_i": round(self.loop_i_w, 1),
|
||||
"loop_lead": round(self.loop_i_lead, 1),
|
||||
"loop_reason": self.loop_reason,
|
||||
}
|
||||
# TEL-11 ⚠️ THE KEY IS PRESENT WITH THE VALUE `None` WHEN THERE IS NO
|
||||
# READING, AND ABSENT ENTIRELY WHEN THERE IS NO SOURCE.
|
||||
# AND THE GATE IS `optional_keys()`, THE SAME EXPRESSION THE
|
||||
# ANNOUNCEMENT USES.
|
||||
# TEL-11. ⚠️ THE KEY IS PRESENT WITH `None` WHEN THERE IS NO READING
|
||||
# AND ABSENT WHEN THERE IS NO SOURCE: two states
|
||||
# (`mqtt.ABSENT_AS_UNKNOWN` makes the first an `unknown`, a gap in the
|
||||
# recorder). TEL-12: still ONE number, the SITE total, None unless
|
||||
# EVERY entity answered. ⚠️ THE GATE IS `optional_keys()`, THE SAME
|
||||
# EXPRESSION THE ANNOUNCEMENT USES.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if "pv" in optional_keys(self.o):
|
||||
values["pv"] = (None if self.pv_w is None
|
||||
else round(self.pv_w, 1))
|
||||
# ⚠️ ONLY when P1 ingestion is actually running.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# ⚠️ ONLY when P1 ingestion is running: the ESP32's stale-input
|
||||
# watchdog subscribes to sensor.goodwe_p1_sample_age and forces layer-1
|
||||
# failsafe at max_age_s, so with meter_source off a published age would
|
||||
# climb unbounded and pin every existing install at 0 W within 30 s.
|
||||
# Publishing nothing leaves the entity non-existent (firmware
|
||||
# has_state()). (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if self.p1_enabled:
|
||||
# Recomputed here, once a second, on purpose - see P1Ingest.
|
||||
values["p1_age"] = round(self.p1.published_age_s, 1)
|
||||
# TEL-07. THE PER-UNIT ROWS, AND THE EMPTY LIST IS THE GATE.
|
||||
# THERE IS NO `if len(self.inverters) > 1` HERE, DELIBERATELY.
|
||||
# TEL-07. THE PER-UNIT ROWS, AND THE EMPTY LIST IS THE GATE: ⚠️ no `if
|
||||
# len(self.inverters) > 1` here, deliberately: `announced_units()`
|
||||
# answers [] at N=1, so what is announced and what is published come
|
||||
# from one expression. ⚠️ The site rows do not move: `battery` stays
|
||||
# the SUM and `soc` the EMPTIEST pack (live dashboards use those ids).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
names = announced_units([inv.name for inv in self.inverters])
|
||||
for inv, slug in zip(self.inverters, unit_slugs(names)):
|
||||
@@ -5076,8 +5095,11 @@ class Controller:
|
||||
for inv in self.inverters))
|
||||
|
||||
async def command_site_zero(self, *, last: bool = False) -> None:
|
||||
"""Every inverter to 0 W, and every inverter's TARGET to 0 W with it.
|
||||
THE SECOND HALF IS NOT BOOKKEEPING, AND IT IS A HAZARD DEV-02 CREATED.
|
||||
"""Every inverter to 0 W, and every inverter's TARGET with it. ⚠️ NOT
|
||||
BOOKKEEPING (DEV-02): the heartbeat re-emits `inv.target`, so zeroing
|
||||
only `self.target` lets the next heartbeat put each unit's last share
|
||||
back on the wire. ⚠️ 0 W IS THE ONE VALUE THAT MAY BE WRITTEN SITE-WIDE
|
||||
(`write_setpoint()` refuses others: ARCH-01).
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
self.target = 0.0
|
||||
for inv in self.inverters:
|
||||
@@ -5088,8 +5110,8 @@ class Controller:
|
||||
def checks(self, when: datetime | None = None) -> list:
|
||||
"""The status rows. `when` is the reference time for anything billed by
|
||||
the calendar - today's month when it is None, which is what the live
|
||||
page means; `status_payload()` calls this with no argument.
|
||||
IT EXISTS SO THE PEAK ROW CAN BE GRADED (TEST-17).
|
||||
page means. ⚠️ IT EXISTS SO THE PEAK ROW CAN BE GRADED (TEST-17): a
|
||||
suite that banks an August quarter went red at the September boundary.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
o = self.o
|
||||
out = []
|
||||
@@ -5143,7 +5165,10 @@ class Controller:
|
||||
"text": messages.row_meter(
|
||||
self.p1.net_w, age, self.p1.parse_errors,
|
||||
unchanged)})
|
||||
# SAFETY-18. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-18. Reported in both states (a detector nobody can see is
|
||||
# armed is not trusted when it fires). ok=False/warn=False so the
|
||||
# banner goes red; it changes NOTHING about control (see
|
||||
# check_meter_frozen()). (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if self.freeze_wd.active:
|
||||
# The figures are in `freeze_wd.reason`, on the log and the
|
||||
# event list (check_meter_frozen()).
|
||||
@@ -5172,19 +5197,19 @@ class Controller:
|
||||
_rail = rail_w(self.site_max_w,
|
||||
dev_min=self.site_dev_bound("dev_min"),
|
||||
dev_max=self.site_dev_bound("dev_max"))
|
||||
# And when the device is what set that number, SAY SO - the
|
||||
# operator configured `max_w` and is owed the reason the guard
|
||||
# is armed somewhere else. ⚠️ Either bound may be None on its
|
||||
# own - `hass.limits()` reads `min` and `max` independently and
|
||||
# returns None for whichever attribute is missing or
|
||||
# unparseable - so this renders whatever is there rather than
|
||||
# formatting a None. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# And when the device set that number, SAY SO: the operator
|
||||
# configured `max_w`. ⚠️ Either bound may be None on its own
|
||||
# (`hass.limits()` reads min and max independently), so render
|
||||
# what is there. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
out.append({"ok": True, "warn": False, "id": "freeze_check",
|
||||
"text": messages.row_freeze_on(
|
||||
self.freeze_wd.cfg.hold_s, _rail,
|
||||
own_limit=_rail < rail_w(self.site_max_w))})
|
||||
|
||||
# TARIFF-01. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# TARIFF-01. The month's billed peak, and a DIFFERENT row when the
|
||||
# store could not answer: "no peak yet" is normal on the 1st,
|
||||
# "unreadable" is a broken instrument.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_pk = self.peak.peak(when)
|
||||
if _pk.is_unreadable:
|
||||
out.append({"ok": False, "warn": True, "id": "month_peak",
|
||||
@@ -5206,9 +5231,10 @@ class Controller:
|
||||
self.peak.recorded)})
|
||||
|
||||
# SAFETY-07. Three rows, not one, because "idle" and "cannot see"
|
||||
# are opposite states and a page that spelled them the same way
|
||||
# would show a green line over a guard that is switched off.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# are opposite states and a page that spelled them the same would
|
||||
# show a green line over a guard that is switched off. `None` is a
|
||||
# fourth state on purpose: before the first cycle nothing has been
|
||||
# concluded. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_pg = self.peak_guard
|
||||
if _pg is None:
|
||||
out.append({"ok": True, "warn": True, "id": "peak_guard",
|
||||
@@ -5220,9 +5246,12 @@ class Controller:
|
||||
elif _pg.floor_limited and (
|
||||
not ((_pg.floor_w or 0.0) > 0.0 or _pg.within_aim)
|
||||
or self._peak_floor_limited):
|
||||
# SAFETY-24. NOT ok, like saturation: in both states the money
|
||||
# is going to be spent and a human is the only actuator left.
|
||||
# SAFETY-25.
|
||||
# SAFETY-24. NOT ok, like saturation (the money is being spent
|
||||
# and a human is the only actuator left); a separate row
|
||||
# because the remedy is opposite. ⚠️ SAFETY-25: the scope
|
||||
# sentence (the floor binds the guard, not the loop) is part of
|
||||
# the row, which carries its own short `banner`; shortening the
|
||||
# ROW would weaken AC 3.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
out.append({"ok": False, "warn": False, "id": "peak_guard",
|
||||
"runs": True,
|
||||
@@ -5262,7 +5291,10 @@ class Controller:
|
||||
"text": _pg.explain()})
|
||||
else:
|
||||
# ⚠️ OUTSIDE the `p1_enabled` block, and that placement is the
|
||||
# point. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# point: SAFETY-07 is NOT COVERED on the legacy single-entity path
|
||||
# (no telegrams), and a row that rendered only where the guard runs
|
||||
# would leave the unprotected install showing nothing.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
out.append({"ok": False, "warn": True, "id": "peak_guard",
|
||||
"text": messages.row_peak_guard_off()})
|
||||
|
||||
@@ -5288,9 +5320,11 @@ class Controller:
|
||||
out.append({"ok": False, "warn": False, "id": "sensor",
|
||||
"text": messages.row_sensor_unset(label, unit)})
|
||||
elif value is None:
|
||||
# ⚠️ SAFETY-56. THIS ROW ASSERTED A FALSE FACT ABOUT A NAMED
|
||||
# ENTITY, WHICH IS THE WORST THING A DIAGNOSTIC CAN DO.
|
||||
# AND IT IS FIXED BY SAYING LESS, NOT BY LOOKING HARDER.
|
||||
# ⚠️ SAFETY-56. THIS ROW ONCE ASSERTED A FALSE FACT ABOUT A
|
||||
# NAMED ENTITY: `Freshness.reading()` returns None BY DESIGN
|
||||
# while the gate withholds a numeric reading. ⚠️ FIXED BY
|
||||
# SAYING LESS: the gate has its own surface (`stale_note()` in
|
||||
# `liveness_notes()`).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
out.append({"ok": False, "warn": False, "id": "sensor",
|
||||
"text": messages.row_sensor_unusable(label, unit)})
|
||||
@@ -5311,7 +5345,11 @@ class Controller:
|
||||
"text": messages.row_control(inv.dev_min,
|
||||
inv.dev_max, unit)})
|
||||
|
||||
# DEV-02. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# DEV-02. Whether this controller can still put a number on each
|
||||
# inverter: a dropped link that shows nothing on the page reads as a
|
||||
# healthy site. Red only for a unit no longer live; the rest amber. The
|
||||
# same notes, in the same order, as `liveness_notes()`.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_now = datetime.now(timezone.utc)
|
||||
for inv in self.inverters:
|
||||
for note, red in ((inv.link_note(_now), not inv.is_live(_now)[0]),
|
||||
@@ -5324,7 +5362,11 @@ class Controller:
|
||||
"text": messages.row_readings_late(
|
||||
self.answer.shown_s, self._late_zero)})
|
||||
|
||||
# SAFETY-03. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-03. Red while an over-rating clamp is engaged (amber for the
|
||||
# integrator bound, SAFETY-40), and the engagement COUNT stays shown
|
||||
# after it clears. Each count is shown under ITS alarm's name, never
|
||||
# the two summed (UX-07/UX-08).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_cw = self.clamp_wd
|
||||
if _cw.active and _cw.bug:
|
||||
out.append({"ok": False, "warn": False, "id": "power_too_high",
|
||||
@@ -5359,10 +5401,18 @@ class Controller:
|
||||
None if len(self.inverters) == 1
|
||||
else inv.name)})
|
||||
|
||||
# SAFETY-02. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-02. Reported in both states. ⚠️ ONE ROW PER INVERTER: a trip
|
||||
# on B leaves A commanding. Since SAFETY-30 the row says what the unit
|
||||
# was compared against (`sign_wd.against`, the string the trip sentence
|
||||
# uses); at N=1 the wording is unchanged.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
for inv in self.inverters:
|
||||
unit = None if len(self.inverters) == 1 else inv.name
|
||||
# ⚠️ ADDED ONLY WHEN THERE IS SOMETHING TO DISAMBIGUATE.
|
||||
# ⚠️ ADDED ONLY WHEN THERE IS SOMETHING TO DISAMBIGUATE: at N=1 the
|
||||
# two sentences below are the ones that have always been on the
|
||||
# page, word for word (AC 8); at N>=2 the noun is load-bearing, so
|
||||
# the row does not send an operator to P1 for a fault visible only
|
||||
# on one inverter's own power.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if inv.sign_wd.tripped:
|
||||
out.append({"ok": False, "warn": False, "id": "direction",
|
||||
@@ -5373,8 +5423,9 @@ class Controller:
|
||||
"text": messages.row_direction_check(
|
||||
held, inv.sign_wd.cfg.hold_s, unit)})
|
||||
|
||||
# SAFETY-30: the SITE row ⚠️ ONLY AT N>=2, and the absence at N=1 is
|
||||
# the honest report rather than a missing row.
|
||||
# SAFETY-30: the SITE row. ⚠️ ONLY AT N>=2, and the absence at N=1 is
|
||||
# the honest report: with one inverter the site comparison and the row
|
||||
# above are the same sentence about the same actuator.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if len(self.inverters) > 1:
|
||||
if self.site_sign_wd.tripped:
|
||||
@@ -5386,8 +5437,10 @@ class Controller:
|
||||
"text": messages.row_site_direction_check(
|
||||
_sheld, self.site_sign_wd.cfg.hold_s)})
|
||||
|
||||
# SAFETY-31 ⚠️ SITE-LEVEL, WHERE THE ROWS ABOVE ARE PER-INVERTER, and
|
||||
# the row says so. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-31. ⚠️ SITE-LEVEL, WHERE THE ROWS ABOVE ARE PER-INVERTER, and
|
||||
# the row says so: there is one circulating figure for the site, and
|
||||
# naming an inverter would invite clearing "the other one".
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_circ = self.site_circulating_w
|
||||
if self.circ_wd.tripped:
|
||||
out.append({"ok": False, "warn": False, "id": "circulating",
|
||||
@@ -5405,9 +5458,12 @@ class Controller:
|
||||
"text": messages.row_circulating_check(
|
||||
_circ, held, self.circ_wd.cfg.hold_s)})
|
||||
|
||||
# SAFETY-42 ⚠️ PER INVERTER, AND WARN RATHER THAN BAD, WHICH IS THE
|
||||
# ONE PLACE THE "reports, does not act" DECISION IS VISIBLE TO AN
|
||||
# OPERATOR. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-42. ⚠️ PER INVERTER, AND WARN RATHER THAN BAD, WHICH IS THE
|
||||
# ONE PLACE THE "reports, does not act" DECISION IS VISIBLE: every
|
||||
# other latched row means the site is at 0 W, this one means it is
|
||||
# STILL RUNNING and quietly short by one unit's share; `ok: False` with
|
||||
# `warn: True` is this list's caution.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if len(self.inverters) > 1:
|
||||
for inv in self.inverters:
|
||||
if inv.delivery_wd.tripped:
|
||||
|
||||
Reference in New Issue
Block a user