Rule 2 graded each battery against last cycle's command and graded charges too. On plantsim's battery (10 s lag) at the 2 s meter it warned at every drain or charge start and 15 times an hour under the peak guard at N=2; a charge tapering near full warned 11 times in 50 min (SAFETY-42 section 7). Now a discharge is graded against the least it stood at over SHORT_CYCLES / SHORT_SPAN_S. DOCS lists `mode-bound` under Loop reason. test_safety85 part 7: peak guard with a dead battery, lagging plant, taper; part 3: the bound ends with maintenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.4 KiB
SAFETY-85 - no false "not keeping up" while another owner sets the battery
Status: implemented 2026-10-01, for review. Tests: test_safety85.py.
The defect. "Battery is not keeping up" (SAFETY-40, goodwe_integrator_bound)
fired about every 2.5 min during maintenance (drain, charge, HOLD) and under the
peak guard, while the battery gave exactly what it was told. Each warning
re-created the HA notification. Rig counts: 24 in an hour (SAFETY-83 F1), 43 in
7 episodes (SAFETY-86), 35 in 16 (SAFETY-87), 12 in 6 (SAFETY-84). The page
counter counted cycles (861), not episodes.
Cause, measured on the real Controller.
- N=1: when a higher claim decides,
cycle()re-seeds the integrator from the target each cycle, thencompute()addsgain * errorfor a grid error the loop does not own. Target + kick crosses the rail and setsi_clamped. The carried integrator is not wound (SAFETY-48 caps the lead at one slew step), so control comes back one slew step at a time. - N>=2: a maintenance pin is a unit claim. The loop stays the site winner and
winds to its rail for watts the pinned battery may not give. When the pin
lifts, the site jumps: 2500 W in one cycle when a HOLD ends under a 4500 W
house, 1390 W when a charge pin lifts (
s84 n2_charge_dear, t=1800 s).
Decision. Three rules; the alarm, its text and its id are unchanged.
- The loop's clamp counts only on a cycle the loop decided. The winner is
the loop. Its number is on the wire, or only a rating (
device,supervised) cut it. No bound held it, and nothing was unmet. (Controller._loop_alarm_input.) - A battery another owner set to discharge alarms when it gives less than
it was told. This covers every battery on a cycle the loop did not
decide, and a battery with its own pin otherwise. The test is more than
saturation_wshort of the least it stood at overSHORT_CYCLEScycles andSHORT_SPAN_S(the plant's measured settle, SAFETY-13/73), forsaturation_cyclescycles in a row (clamp.delivery_short). It covers a real shortfall during maintenance or under the peak guard. Before, that shortfall was silent unless the kick happened to reach the rail (house 1000 W, drain 2000 W, battery gives nothing: 0 warnings before, 4 in 10 min now). - At N>=2, while maintenance holds a battery, the loop is bounded by
OPT-11's
loop_bound()(what the batteries may place). It does not wind, and control comes back one slew step a cycle. Why maintenance only: that is where the jump was measured and where the owner order puts maintenance above the loop. Other N>=2 limits (pack cut-out, SAFETY-73 refusal) keep the old law. Rule 1 still silences them (the target is unmet), so they no longer warn either.
Fixing the windup was chosen over only ignoring the wound part, because at N>=2 the wound integrator moves real watts at hand-back. At N=1 there was nothing to fix: re-seeding already prevents windup.
What SAFETY-03/40 protect is kept. With the loop in charge, the integrator bound still warns (a runaway, a house above the rating), and a saturated inverter under the loop's own command still does not alarm (SAFETY-03 AC 2): rule 2 grades only batteries another owner set.
The counter. ClampWatchdog.bound_episodes counts episodes that had a
bound engagement. The status row shows it ("N times since start").
bound (cycles) stays for the log and the tests.
Re-pinned digests. Every change is intended; nothing at N=1 moves a watt.
| digest | what changed |
|---|---|
s78 n1_drain_owner |
the false clamp rows are gone from the log; commands identical |
s78 n2_drain, s84 n2_charge, n2_charge_banked, s79 n2_maint (test_peak_margin) |
loop reason (mode-bound), integrator and loop claim while bounded; commands identical |
opt11 n2_maint (test_modes) |
A drops out at t=1655 s: B follows the house at once, 8 cycles differ (max 920 W) |
s84 n2_charge_dear |
A's pin lifts at t=1800 s: 400 W a cycle, not a 1390 W jump; the surplus step after it; 98 cycles |
test_safety86's "without the ceiling the log floods" now needs the ceiling
and the bound both removed: either one alone keeps the loop inside what the
batteries can give. The alarmed() helpers in test_safety84/86 also read
SAFETY-81's quarter stamp, because ALLOCATOR rows can push its row out of
the 40-row log.
Review (PR #163). The first draft graded each battery against last
cycle's command and graded charges too. On plantsim's battery (10 s lag) at
the live 2 s meter, every drain or charge start warned once, and an N=2 drain
or HOLD under the peak guard with a stepping house warned 15 times an hour. A
maintenance charge tapering near full warned 11 times in 50 min, which
SAFETY-42 section 7 had ruled out. With the settle window and discharge only:
0 in all of those cases. A battery that gives nothing still warns, lagging
or not. At a hypothetical 20 s lag, it warns at most twice an hour. The cost:
a battery that does not take a charge another owner set is not named by this
alarm (the same gap SAFETY-42 states; the charge has its 36 h timeout).
test_safety85.py part 7.
Not changed, open for the owner.
- With the loop in charge and the house above the batteries' total rating, it still warns (SAFETY-40). On the rig's 2 x 2000 W N=2, most of SAFETY-84's count came from houses of 4500 and 6500 W.
- TEL-10's
Loop reasonreadsmode-boundwhile the maintenance bound holds the loop, with modes off. DOCS lists the value with both meanings.