SAFETY-136: a gentler loop on a slow meter until the last move shows
checks / unit (pull_request) Successful in 26m18s
infra-pin / fresh (pull_request) Successful in 2s

On a meter whose readings come 4 s or more apart, the loop runs at half its
gain after a move of 150 W or more, until the meter has fallen by half of the
move or 2 cycles have passed. With readings 10 s late on a 5 s meter the export
falls 44 % over oven, kettle and hob, owner shape, steps; 1-2 s meters are
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
goodwe-agent
2026-11-09 14:49:26 +01:00
parent 8508ac33f0
commit c50e2279dd
8 changed files with 398 additions and 8 deletions
+14 -6
View File
@@ -734,15 +734,21 @@ class CycleMixin:
# TEL-10. ⚠️ ONE NAME FOR THE COMMAND THE LOOP WAS MEASURED
# AGAINST. (DECISIONS/MAIN-orchestration-archive.md)
loop_prev = self.target
# SAFETY-95: a drain's tail is not the loop's to chase.
# OPT-15: `loop_grid` is the grid, or the target while a run
# edge is held.
grid_in = (self._drain_grid_w(_live96) if self._drain_tail
else loop_grid)
# SAFETY-136: on a slow meter, a gentler loop until the last move
# shows.
slow_meter = self.settle.slow()
if moved or mode_trade:
self.settle.reset()
decision = compute(
prev_w=loop_prev,
# SAFETY-95: a drain's tail is not the loop's to chase.
# OPT-15: `loop_grid` is the grid, or the target while a run
# edge is held.
grid_w=(self._drain_grid_w(_live96) if self._drain_tail
else loop_grid),
grid_w=grid_in,
actual_w=site_batt,
tuning=self.tuning,
tuning=self.settle.tuning(self.tuning, grid_in, slow_meter),
sat_count=self.sat_count,
i_w=self.i_w,
# SAFETY-52 at one inverter only: with several, OPT-09's pack
@@ -772,6 +778,7 @@ class CycleMixin:
self.log_event(f"saturation freeze ({self.target:.0f} W vs "
f"{site_batt:.0f} W)",
hint=messages.hint("not_keeping_up"))
self.settle.moved(grid_in, loop_prev, decision.target_w, slow_meter)
self.sat_count = decision.sat_count
self.i_w = decision.i_w
self.loop_w = decision.target_w
@@ -811,6 +818,7 @@ class CycleMixin:
else:
# Stopped or blind: no accumulation may survive the outage.
self.i_w = 0.0
self.settle.reset()
self.loop_w = None
self._live_was = frozenset(_live96)
# TEL-10, and it is the same zero the line above writes rather than