SAFETY-93 review: a charge that ends by a stale link or a refusal hands back smoothly too
A battery still in CHARGE but dropped by the allocator (stale link, or a SAFETY-73 refusal contradicting its charge) left its -2000 W in the loop's start point, and B went 0 -> -1600 W in one cycle. The end is now noted after the unit claims are filed: in CHARGE, live and not contradicted. Tests: part 6 (stale link, refusal) and a spy on the guard suspension. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -994,8 +994,8 @@ class Controller:
|
||||
self._drain_waits = set()
|
||||
# SAFETY-91: last cycle's held-back batteries (`_held_back()`).
|
||||
self._held_was = frozenset()
|
||||
# SAFETY-93: the batteries in their maintenance charge last cycle,
|
||||
# and those whose charge ended since the loop last ran.
|
||||
# SAFETY-93: the batteries placed in their maintenance charge last
|
||||
# cycle, and those whose charge ended since the loop last ran.
|
||||
self._charging_was = frozenset()
|
||||
self._charge_ended = frozenset()
|
||||
# Set by amain() once the transport is built, so the status page can show
|
||||
@@ -2372,6 +2372,21 @@ class Controller:
|
||||
self.target = want
|
||||
self.i_w = want - self.tuning.gain * (want - float(batt))
|
||||
|
||||
def _charge_track(self, claims, maint_results) -> None:
|
||||
"""SAFETY-93, N>=2: a charge ends when its battery leaves CHARGE or can
|
||||
no longer be placed (stale link, refusal). Kept until the loop runs."""
|
||||
if len(self.inverters) < 2:
|
||||
return
|
||||
live = set(self.live_units())
|
||||
now = set()
|
||||
for inv, res in zip(self.inverters, maint_results):
|
||||
u = self.actuator_of("maintenance", inv)
|
||||
if res.phase == CHARGE and u in live and not pre_resolve(
|
||||
[c for c in claims if c.actuator == u]).contradiction:
|
||||
now.add(inv.name)
|
||||
self._charge_ended |= self._charging_was - now
|
||||
self._charging_was = frozenset(now)
|
||||
|
||||
def _reseed_charge_end(self, maint_results) -> None:
|
||||
"""SAFETY-93, N>=2: when a battery's maintenance charge ends, the loop
|
||||
starts from what the others stood at plus that battery's new command
|
||||
@@ -3603,11 +3618,6 @@ class Controller:
|
||||
self.log_event(msg, hint=messages.hint("maintenance",
|
||||
phase=machine.phase))
|
||||
maint_results.append(res)
|
||||
# SAFETY-93: kept until the loop next runs (a missed poll holds).
|
||||
_charging = frozenset(inv.name for inv, r in zip(
|
||||
self.inverters, maint_results) if r.phase == CHARGE)
|
||||
self._charge_ended |= self._charging_was - _charging
|
||||
self._charging_was = _charging
|
||||
# ⚠️ CARRIED TO THE STATUS PAGE, NOT LEFT IN THE 40-LINE ACTION LOG. The
|
||||
# deferral is edge-logged (it lasts as long as the other pack's cycle),
|
||||
# so by the time an operator asks "why has pack B not recalibrated" the
|
||||
@@ -4583,6 +4593,7 @@ class Controller:
|
||||
if self.modes_on:
|
||||
mode_trade = self._mode_trade(claims, now)
|
||||
self._mode_trade_was = mode_trade
|
||||
self._charge_track(claims, maint_results)
|
||||
if (self.auto or self.shadow) and self.inputs_ok:
|
||||
# ⚠️ prev is the ARBITER's last output, not the loop's own last
|
||||
# wish. If something outranked the loop, that is what the hardware
|
||||
|
||||
Reference in New Issue
Block a user