MAINT-20 part 2 r2: app/main.py comments, lines 5501-6000 (status_payload .. maint_site_tz)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
b05ad2b273
commit
40ef27c367
@@ -5508,11 +5508,11 @@ class Controller:
|
||||
out.append({"ok": False, "warn": True, "id": "solar_forecast",
|
||||
"text": messages.row_forecast_gap(
|
||||
day, gone, len(ents))})
|
||||
# TEL-11 ⚠️ NO ROW AT ALL WHEN THE ROSTER IS EMPTY, AND THAT IS A
|
||||
# DELIBERATE DIFFERENCE FROM THE PEAK-FORECAST CAUTION DIRECTLY ABOVE.
|
||||
# AND WHEN IT IS SET, A FAILED READ IS `ok=False, warn=True` - a
|
||||
# CAUTION, never `bad`.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# TEL-11. ⚠️ NO ROW AT ALL WHEN THE ROSTER IS EMPTY (pure
|
||||
# instrumentation: no warning spam). ⚠️ WHEN IT IS SET, A FAILED READ
|
||||
# IS `ok=False, warn=True`, a CAUTION never `bad`: telemetry must not
|
||||
# put a battery site into NOT READY. UX-08 replaced 0.11.0's sentences
|
||||
# on purpose. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
roster = self.pv_roster
|
||||
if roster:
|
||||
# UX-08: sources are numbered in the order of the settings list,
|
||||
@@ -5566,31 +5566,35 @@ class Controller:
|
||||
runs = [c for c in bad if c.get("runs")]
|
||||
bad = [c for c in bad if not c.get("runs")]
|
||||
running_alarm = waiting = False
|
||||
# SAFETY-10, AND IT IS FIRST IN THE CASCADE "The battery is NOT being
|
||||
# controlled" is the one sentence on this page that may never be
|
||||
# displaced by another condition.
|
||||
# AND IT CARRIES A STANDING LATCH RATHER THAN HIDING ONE.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-10, AND IT IS FIRST IN THE CASCADE: "The battery is NOT being
|
||||
# controlled" may never be displaced by another condition. ⚠️ AND IT
|
||||
# CARRIES A STANDING LATCH RATHER THAN HIDING ONE: `shadow.banner()`
|
||||
# names the latch and goes red for it, so nothing the cascade would
|
||||
# have said is lost. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_shadow = shadow.banner(self.o, control_started=self.auto,
|
||||
latched=self.shadow_latches())
|
||||
if _shadow is not None:
|
||||
level, banner = _shadow
|
||||
elif self.circ_wd.tripped:
|
||||
# ⚠️ ABOVE THE SIGN FAILSAFE, and the order is a decision. Both are
|
||||
# latched P_SAFETY stops, so the site is at 0 W either way and the
|
||||
# banner can only carry one sentence.
|
||||
# ⚠️ ABOVE THE SIGN FAILSAFE, and the order is a decision: both are
|
||||
# latched P_SAFETY stops and the banner carries one sentence; this
|
||||
# one goes first because it is the fault the operator has NO OTHER
|
||||
# WAY of learning about (a sign disagreement is visible at the
|
||||
# meter, circulating power nowhere).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
level, banner = "bad", messages.banner_circulating()
|
||||
elif self.site_sign_wd.tripped and len(self.inverters) > 1:
|
||||
# SAFETY-30's site half, ABOVE the per-unit rows below and below
|
||||
# circulating power above, and the ordering is the same argument
|
||||
# both times: the banner carries one sentence, so it carries the
|
||||
# one that says the widest true thing.
|
||||
# circulating power above: the banner carries one sentence, so the
|
||||
# one that says the widest true thing. A site sign trip parks EVERY
|
||||
# inverter and the fault is in what the site was asking for; naming
|
||||
# a unit would send an operator to swap the wrong box.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
level, banner = "bad", messages.banner_site_direction()
|
||||
elif any(i.sign_wd.tripped for i in self.inverters):
|
||||
# Outranks "Not ready": nothing is missing, something went wrong.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# At N>=2 the unit was graded against its own power, so the banner
|
||||
# says so (see checks()). (DECISIONS/MAIN-orchestration-archive.md)
|
||||
level, banner = "bad", messages.banner_direction(
|
||||
len(self.inverters) > 1)
|
||||
elif bad:
|
||||
@@ -5609,14 +5613,16 @@ class Controller:
|
||||
runs[0]["text"])
|
||||
elif any(i.delivery_wd.tripped for i in self.inverters):
|
||||
# ⚠️ SAFETY-42, AND IT IS A `warn` BANNER RATHER THAN A `bad` ONE,
|
||||
# WHICH IS THE WHOLE "reports, does not act" DECISION IN ONE LINE.
|
||||
# BUT IT MUST DISPLACE "Running — holding grid at zero", AND THAT
|
||||
# IS NOT COSMETIC.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# WHICH IS THE WHOLE "reports, does not act" DECISION IN ONE LINE
|
||||
# (the site is STILL RUNNING, one unit short). ⚠️ BUT IT MUST
|
||||
# DISPLACE "Running - holding grid at zero": that sentence is a
|
||||
# claim the site cannot make while a unit is absent. It sits BELOW
|
||||
# "Stopped". (DECISIONS/MAIN-orchestration-archive.md)
|
||||
level, banner = "warn", messages.banner_not_delivering(
|
||||
[i.name for i in self.inverters if i.delivery_wd.tripped])
|
||||
elif self.maint.phase != IDLE:
|
||||
# ⚠️ THE PACK IS NAMED ONLY WHEN THERE IS MORE THAN ONE.
|
||||
# ⚠️ THE PACK IS NAMED ONLY WHEN THERE IS MORE THAN ONE: at N=1 the
|
||||
# string is what it has always been, character for character.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
level, banner = "warn", messages.banner_maintenance(
|
||||
self.maint.phase,
|
||||
@@ -5627,9 +5633,9 @@ class Controller:
|
||||
def w(v):
|
||||
return "—" if v is None else f"{v:,.0f} W".replace(",", " ")
|
||||
|
||||
# ⚠️ THE EARLIEST ACROSS THE PACKS, NOT THE FIRST PACK'S. One date on a
|
||||
# site with two schedules has to be the one that comes first, or the
|
||||
# row says "2026-10-01" while a pack recalibrates tomorrow.
|
||||
# ⚠️ THE EARLIEST ACROSS THE PACKS, NOT THE FIRST PACK'S. `None` means
|
||||
# "never run, due now" and has to win over any date, so it is tested
|
||||
# for rather than sorted with. At N=1 this is that pack's own date.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_now = datetime.now(timezone.utc)
|
||||
_dues = [m.next_due(_now) for m in self.maints]
|
||||
@@ -5639,15 +5645,14 @@ class Controller:
|
||||
("Grid", w(self.grid)),
|
||||
("Battery", w(self.site_batt_w)),
|
||||
# SAFETY-31, and it belongs DIRECTLY UNDER the two figures it is
|
||||
# not derivable from. "Grid 0 W, Battery 0 W, Circulating 6000 W"
|
||||
# is the whole card in three rows: both of the numbers an operator
|
||||
# normally reads say the site is idle, and the third says 6 kW is
|
||||
# going round between the packs.
|
||||
# not derivable from ("Grid 0 W, Battery 0 W, Circulating 6000 W"
|
||||
# is the whole card in three rows). Only shown at N>=2: at one
|
||||
# inverter it is identically zero.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
*(() if len(self.inverters) <= 1
|
||||
else ((("Circulating", w(self.site_circulating_w)),))),
|
||||
# TEL-11, and ONLY when a source is configured - see `checks()`.
|
||||
# `w(None)` renders "—", which is how this row says "no reading"
|
||||
# `w(None)` renders "-", which is how this row says "no reading"
|
||||
# rather than "0 W". (DECISIONS/MAIN-orchestration-archive.md)
|
||||
*(() if not self.pv_roster else ((("Solar production",
|
||||
w(self.pv_w)),))),
|
||||
@@ -5688,16 +5693,18 @@ class Controller:
|
||||
# PRICE-01: only while no price covers now.
|
||||
*((("Prices", messages.row_no_prices(*self._gap_args())),)
|
||||
if self.price_gap else ()),
|
||||
# FCST-04. WHAT §8.3 HAS TO CARRY FORWARD, which is the difference
|
||||
# between a 48-hour plan and a 12-hour one on a weekday morning.
|
||||
# FCST-04. WHAT §8.3 HAS TO CARRY FORWARD, the difference between a
|
||||
# 48-hour plan and a 12-hour one on a weekday morning. Its own row:
|
||||
# "Tariff" says what the plan IS, this says what it could still be
|
||||
# filled from if the feed is late again.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
("Price history", self.price_history.words),
|
||||
# OPT-04. The strategy's own sentence, not a re-derivation of it -
|
||||
# every refusal it can make already carries a reason a human can
|
||||
# act on, and "Arbitrage: no" would throw all of them away.
|
||||
# TARIFF-10 RE-READ THIS STRING AND LEFT IT ALONE UNDER A COMMENT
|
||||
# THAT WAS FALSE, AND TARIFF-11 IS THAT CORRECTION.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# OPT-04. The strategy's own sentence, not a re-derivation: every
|
||||
# refusal it can make carries a reason a human can act on. ⚠️
|
||||
# TARIFF-11: `self.arb is None` is NOT "trading is off" (cycle()
|
||||
# returns at the input hold first); `arbitrage_row()` reads the
|
||||
# option, and `cycle_at`/`arb_at` make "no decision this cycle" an
|
||||
# observation. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
("Arbitrage", arbitrage_row(
|
||||
on=arbitrage.enabled(self.o) or self.modes_on,
|
||||
decision=self.arb,
|
||||
@@ -5705,15 +5712,19 @@ class Controller:
|
||||
now=datetime.now(timezone.utc), why=self.why_words)),
|
||||
]
|
||||
held = level == "bad" and not running_alarm
|
||||
# UX-46. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# UX-46. The page's language is chosen once, here, for the Home
|
||||
# sentences, checks, rows that carry one and the banner; a plain str
|
||||
# (no Dutch twin) stays as it is.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
checks = [dict(c, text=messages.pick(c["text"], lang)) for c in checks]
|
||||
rows = [(k, messages.pick(v, lang)) for k, v in rows]
|
||||
banner = messages.pick(banner, lang)
|
||||
out = {
|
||||
"banner": banner, "level": level, "rows": rows, "checks": checks,
|
||||
"hint": self.hint,
|
||||
# The action log itself, newest first.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# The action log itself, newest first: not shown on the page (UX-08
|
||||
# 2d) but readable in the `status` JSON, since several events are
|
||||
# written nowhere else. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
"events": list(self.events),
|
||||
}
|
||||
# UX-13 P1: the Home screen's structured half. Read-only, and it may
|
||||
@@ -5759,8 +5770,11 @@ class Controller:
|
||||
hint=messages.hint("control_started" if self.auto
|
||||
else "control_stopped"))
|
||||
if self.auto:
|
||||
# ⚠️ THE ONLY WAY OUT OF A SIGN-DISAGREEMENT TRIP, on purpose.
|
||||
# IT CLEARS EVERY INVERTER'S LATCH, AND SAYS SO PER INVERTER.
|
||||
# ⚠️ THE ONLY WAY OUT OF A SIGN-DISAGREEMENT TRIP, on purpose:
|
||||
# it clears on a deliberate human start, not on the condition
|
||||
# going away (the condition ALWAYS goes away, because the trip
|
||||
# commands 0 W). ⚠️ IT CLEARS EVERY INVERTER'S LATCH, AND SAYS
|
||||
# SO PER INVERTER, each under its OWN key.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
for inv in self.inverters:
|
||||
if not inv.sign_wd.tripped:
|
||||
@@ -5773,15 +5787,18 @@ class Controller:
|
||||
hint=messages.hint("stop_cleared",
|
||||
unit=inv.name))
|
||||
# ⚠️ AND THE PRE-DEV-02 FLAT KEYS GO WITH THEM,
|
||||
# UNCONDITIONALLY. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# UNCONDITIONALLY: this is what makes `flat True + namespaced
|
||||
# False` MEAN something, and why `_grade_store_migration()` can
|
||||
# run on every start. Left behind, a flat key is a fossil that
|
||||
# re-arms the failsafe on every restart.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
self.store.set("sign_tripped", False)
|
||||
self.store.set("sign_reason", "")
|
||||
# SAFETY-30's SITE latch, cleared by the SAME press and for the
|
||||
# same reason the per-unit ones are: the condition always goes
|
||||
# away on its own, because the trip commands 0 W everywhere and
|
||||
# a site commanding zero cannot oppose anything.
|
||||
# UNCONDITIONAL, LIKE THE FLAT PAIR ABOVE, AND NOT GUARDED ON
|
||||
# THE ROSTER.
|
||||
# SAFETY-30's SITE latch, cleared by the SAME press for the
|
||||
# same reason (the trip commands 0 W, so the condition goes
|
||||
# away on its own). ⚠️ UNCONDITIONAL, NOT GUARDED ON THE
|
||||
# ROSTER: an install reconfigured back to one inverter would
|
||||
# otherwise keep `site_sign_tripped: true` for ever.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if self.site_sign_wd.tripped:
|
||||
self.log_event("site sign-disagreement failsafe cleared "
|
||||
@@ -5790,12 +5807,10 @@ class Controller:
|
||||
self.site_sign_wd.reset()
|
||||
self.store.set("site_sign_tripped", False)
|
||||
self.store.set("site_sign_reason", "")
|
||||
# SAFETY-31, cleared by the SAME press and for the same reason:
|
||||
# the condition always goes away on its own, because the trip
|
||||
# commands 0 W everywhere and two inverters at zero cannot
|
||||
# oppose.
|
||||
# AND `_circ_announced` GOES WITH IT.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-31, cleared by the SAME press and for the same reason.
|
||||
# ⚠️ AND `_circ_announced` GOES WITH IT: left True, the NEXT
|
||||
# trip in this process would latch, stop the site and raise no
|
||||
# notification. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if self.circ_wd.tripped:
|
||||
self.circ_wd.reset()
|
||||
self.store.set("circ_tripped", False)
|
||||
@@ -5803,9 +5818,12 @@ class Controller:
|
||||
self.log_event("circulating-power trip cleared by operator",
|
||||
hint=messages.hint("site_stop_cleared"))
|
||||
self._circ_announced = False
|
||||
# SAFETY-42, cleared by the SAME press, and for a reason that
|
||||
# is NOT the one above.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# SAFETY-42, cleared by the SAME press, for a reason that is
|
||||
# NOT the one above: this latch commands nothing, so its
|
||||
# evidence still stands and it could clear itself. It does not:
|
||||
# a unit that stopped delivering and resumed is an INTERMITTENT
|
||||
# fault. ⚠️ AND `_delivery_announced` GOES WITH IT, like
|
||||
# `_circ_announced`. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
for inv in self.inverters:
|
||||
if inv.delivery_wd.tripped:
|
||||
self.log_event(f"not-delivering report on {inv.name} "
|
||||
@@ -5829,7 +5847,11 @@ class Controller:
|
||||
return await self._maint_unit_action(what, unit, now)
|
||||
if what == "maint_start":
|
||||
# ⚠️ WHICH PACK, BECAUSE THERE ARE NOW SEVERAL AND THE BUTTON HAS
|
||||
# ONE LABEL. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# ONE LABEL: `_maint_button()` picks the pack that holds the
|
||||
# maintenance slot, else the one longest without a cycle; at N=1
|
||||
# both are `maints[0]`. This is the bare request; a named battery
|
||||
# is `_maint_unit_action()` (MAINT-11).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
_m = self._maint_button(now)
|
||||
for msg in _m.force_start(now):
|
||||
self.log_event(msg, hint=messages.hint("maintenance",
|
||||
@@ -5874,7 +5896,10 @@ class ControlTaskDied(RuntimeError):
|
||||
|
||||
|
||||
def mode_announcement(opts, store) -> tuple:
|
||||
"""OPT-11 P2: `(modes, retract)` for the MQTT publisher at start-up.
|
||||
"""OPT-11 P2: `(modes, retract)` for the MQTT publisher at start-up. The
|
||||
config topics announced are kept in the store, so turning modes off removes
|
||||
exactly those (`mqtt.mode_configs()`); an install that never turned modes
|
||||
on gets nothing, and its store is not written.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
modes = packmode.MODES if packmode.enabled(opts) else ()
|
||||
stored = store.data.get(MODE_CONFIGS_KEY)
|
||||
@@ -5907,6 +5932,7 @@ DOCS_LINK_TIMEOUT_S = 3
|
||||
|
||||
async def docs_link_facts(session, token: str) -> dict:
|
||||
"""`{slug, ha_version}` from the Supervisor, each None when unreadable.
|
||||
Asked together with a 3 s budget: a link may never hold up control start.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
async def ask(url, field):
|
||||
"""`(value, why)`: the field, or None and a short reason (UX-09)."""
|
||||
|
||||
Reference in New Issue
Block a user