MAINT-20 part 2 r2: app/main.py comments, lines 1001-1500 (_refusal_from_stale .. site_circulating_w)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
f114a19dcc
commit
6492d179d6
@@ -1191,22 +1191,13 @@ class Controller:
|
||||
|
||||
def _grade_store_migration(self, store) -> None:
|
||||
"""DEV-02. Did SAFETY-02's latch actually survive the key rename?
|
||||
|
||||
Graded, not performed: a latch that silently fails to move is a
|
||||
DISARMED SAFETY GUARD on an install that believes it is protected
|
||||
(state file says `sign_tripped: true`, banner green, controller resumes
|
||||
into the fault). The pre-DEV-02 flat value is compared with what the
|
||||
roster came up with; a disagreement re-arms the latch by hand from the
|
||||
flat value and writes the store. The fail-safe direction for a latch is
|
||||
ON.
|
||||
|
||||
DISARMED SAFETY GUARD on an install that believes it is protected. The
|
||||
pre-DEV-02 flat value is compared with what the roster came up with; a
|
||||
disagreement re-arms the latch by hand (the fail-safe direction is ON).
|
||||
It runs on EVERY start, not only the migrating one: gating on `rep.ran`
|
||||
swallowed store.py's rule-2 round trip (upgrade, DOWNGRADE with a trip
|
||||
in the window, upgrade again: the namespaced key says untripped, the
|
||||
flat key says tripped; performed on this site 2026-09-02). The gate
|
||||
could go because `handle_action()` now clears the flat keys with the
|
||||
namespaced ones and the trip mirrors them, so FLAT TRIPPED AND NO UNIT
|
||||
TRIPPED means a downgrade-window trip or a lost migration.
|
||||
swallowed store.py's rule-2 round trip (upgrade, downgrade with a trip
|
||||
in the window, upgrade; performed on this site 2026-09-02).
|
||||
DECISIONS/MAIN-orchestration.md."""
|
||||
rep = self.store_migration
|
||||
if rep.ran and not rep.ok:
|
||||
@@ -1218,24 +1209,15 @@ class Controller:
|
||||
if not legacy_tripped or not self.inverters:
|
||||
return
|
||||
# The question is "is any namespaced latch set THAT A MACHINE READS"
|
||||
# (SAFETY-44, second half), asked of the STORE's keys, not the roster's
|
||||
# objects: re-arming A off a flat key that B's own trip mirrored would
|
||||
# latch a healthy inverter on every restart (DEV-02's defect).
|
||||
# Three cases: on the roster, a machine reads it, stop. Off-roster and
|
||||
# named `UNIT_A`, the broken migration's fingerprint (the only name it
|
||||
# wrote; `roster_names()` never hands out `inverter-a` otherwise): the
|
||||
# flat latch misdirected, repair onto `inverters[0]`. Off-roster, other
|
||||
# name: a real unit out of today's config; stop, unless a stranded
|
||||
# `UNIT_A` latch is also present. Three variables, not one shared flag
|
||||
# (one key defeated the first fix; MUTATION.md habit 4, instance 17).
|
||||
# This guard is the ONLY repair for sites upgraded before the fix, as
|
||||
# `migrate_unit_keys()` returns at the version check.
|
||||
# Residual: a first unit really NAMED `inverter-a`, latched, then
|
||||
# removed from the roster is re-armed on `inverters[0]` (a healthy unit
|
||||
# held at 0 W; the conservative direction). The reason shown is the
|
||||
# flat mirror's, which carries whatever tripped LAST, so on a
|
||||
# multi-unit site one inverter's evidence may print under another's
|
||||
# name. DECISIONS/MAIN-orchestration.md.
|
||||
# (SAFETY-44), asked of the STORE's keys, not the roster's objects:
|
||||
# re-arming A off a flat key that B's trip mirrored would latch a
|
||||
# healthy inverter on every restart. Three cases: on the roster, stop;
|
||||
# off-roster named `UNIT_A` (the broken migration's fingerprint),
|
||||
# repair onto `inverters[0]`; off-roster, other name: stop unless a
|
||||
# stranded `UNIT_A` latch is present. This guard is the ONLY repair for
|
||||
# sites upgraded before the fix. Residual: a first unit really named
|
||||
# `inverter-a`, latched and removed, is re-armed on `inverters[0]`
|
||||
# (conservative). DECISIONS/MAIN-orchestration.md.
|
||||
on_roster = {inv.name for inv in self.inverters}
|
||||
stranded, on_roster_latched, other_off_roster = [], False, False
|
||||
for k, v in data.items():
|
||||
@@ -1429,20 +1411,15 @@ class Controller:
|
||||
|
||||
# SAFETY-30: which comparison each sign detector is making
|
||||
def _arm_sign_watchdogs(self) -> None:
|
||||
"""Tell every sign detector which of SAFETY-30's two questions it asks.
|
||||
A pure function of `len(self.inverters)` and the only place the
|
||||
roster decides anything about this check (one method, not a
|
||||
conditional at two call sites).
|
||||
At ONE inverter (6.2, AC 8) the unit's detector is armed against THE
|
||||
METER with the shipped pair and the site detector is not armed: the
|
||||
same comparison, thresholds, reason string, keys and `failsafe` claim
|
||||
as before. At TWO OR MORE each unit is armed against ITS OWN measured
|
||||
power (`Inverter.batt`, register 0x0518) under the per-unit pair, and
|
||||
the site detector against the meter under the site pair.
|
||||
Called again at the top of EVERY `cycle()`: fixtures append an
|
||||
`Inverter` after construction, and a detector frozen at construction
|
||||
would be graded against the meter at a roster of two. `arm()` touches
|
||||
no timer or latch, so repeating it is safe."""
|
||||
"""Tell every sign detector which of SAFETY-30's two questions it asks;
|
||||
a pure function of `len(self.inverters)`. At ONE inverter (6.2, AC 8)
|
||||
the unit's detector is armed against THE METER with the shipped pair
|
||||
and the site detector is not armed. At TWO OR MORE each unit is armed
|
||||
against ITS OWN measured power (`Inverter.batt`) under the per-unit
|
||||
pair, and the site detector against the meter under the site pair.
|
||||
Called at the top of EVERY `cycle()` (fixtures append an `Inverter`
|
||||
after construction); `arm()` touches no timer or latch.
|
||||
DECISIONS/MAIN-orchestration.md."""
|
||||
one = len(self.inverters) <= 1
|
||||
for inv in self.inverters:
|
||||
inv.sign_wd.arm(self.sign_cfg if one else self.unit_sign_cfg,
|
||||
@@ -1463,19 +1440,13 @@ class Controller:
|
||||
|
||||
def trade_power_w(self, floor_pct, ceiling_pct) -> tuple:
|
||||
"""OPT-09. `(charge_w, discharge_w)`: how hard a trade may be SIZED:
|
||||
what the site can push AND STILL HAVE SOMEWHERE TO PUT (a roster of
|
||||
two used to size at the sum of ratings, and unplaceable watts came
|
||||
back as `unmet_w` for the integrator to chase).
|
||||
The bounds are arbitrage's own (`arbitrage_soc_floor` / `_ceiling`),
|
||||
NOT the BMS cut-out: sizing a trade is the trade's business; the
|
||||
`packshare.envelope()` pack claim uses the physical cut-out
|
||||
(SAFETY-25). Different questions, different numbers. It sizes the ask,
|
||||
it does not route it: weights are a preference, so a pack between its
|
||||
arbitrage floor and its cut-out can still take part of a sell (a card
|
||||
of its own). A pack whose SoC did not read counts on neither side.
|
||||
Neither side may be zero on a trading tick (`arbitrage.decide()`
|
||||
refuses `IDLE_NO_SIZE`: a 0 W claim is a park order).
|
||||
DECISIONS/MAIN-orchestration.md."""
|
||||
what the site can push AND STILL HAVE SOMEWHERE TO PUT. The bounds are
|
||||
arbitrage's own (`arbitrage_soc_floor` / `_ceiling`), NOT the BMS
|
||||
cut-out that `packshare.envelope()` uses (SAFETY-25): different
|
||||
questions. It sizes the ask, it does not route it. A pack whose SoC did
|
||||
not read counts on neither side. Neither side may be zero on a trading
|
||||
tick (`arbitrage.decide()` refuses `IDLE_NO_SIZE`: a 0 W claim is a
|
||||
park order). DECISIONS/MAIN-orchestration.md."""
|
||||
# The reduction lives in `packshare`; `test_packshare.py` part 9 calls
|
||||
# the SAME function (a transcription in a check is a copy that drifts).
|
||||
return packshare.trade_power_w(self.pack_energy, floor_pct, ceiling_pct)
|
||||
|
||||
Reference in New Issue
Block a user