Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-85-no-false-not-keeping-up.md
T
adminandClaude Opus 5.5 7b723a6cd5
checks / unit (pull_request) Successful in 25m47s
infra-pin / fresh (pull_request) Successful in 2s
SAFETY-85 review: grade discharge only, over the plant's settle time
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>
2026-10-01 20:25:41 +02:00

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, then compute() adds gain * error for a grid error the loop does not own. Target + kick crosses the rail and sets i_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.

  1. 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.)
  2. 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_w short of the least it stood at over SHORT_CYCLES cycles and SHORT_SPAN_S (the plant's measured settle, SAFETY-13/73), for saturation_cycles cycles 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).
  3. 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 reason reads mode-bound while the maintenance bound holds the loop, with modes off. DOCS lists the value with both meanings.