"""SAFETY-136: on a slow meter, follow the meter gently until the last move shows. A meter whose readings come 4 s or more apart, read through Home Assistant, can be 10-15 s late. The loop then answers the same error again at each cycle and overshoots. After a move of MOVE_W or more the loop therefore runs at WAIT_SHARE of its gain until the meter has fallen SHOWN of the move, or for at most WAIT_CYCLES cycles. Off for a meter under 4 s, so those sites run as before. DECISIONS/SAFETY-136-slow-meter-settle.md. """ from collections import deque from dataclasses import replace from .lateness import M_FIT_S, M_ROWS # A meter is slow when its readings come further apart than this (the usual # gap, over the last 60 s and at least 5 readings): the fast meter fit's own # limit (SAFETY-128), 3.75 s. SLOW_GAP_S = M_FIT_S / M_ROWS SLOW_ROWS = 5 # A move this big is waited for (above the house noise of about 80 W). MOVE_W = 150.0 # The meter has shown a move once it fell by this share of it. SHOWN = 0.5 # Meanwhile the loop runs at this share of its gain. WAIT_SHARE = 0.5 # Wait at most this many cycles (10 s at a 5 s meter). WAIT_CYCLES = 2 class Settle: def __init__(self): self.pend = None # (meter at the move, the move in W, cycles waited) self._n = None # readings taken in so far self._at = deque() # when the last ones arrived, over M_FIT_S def arrive(self, now, n): """Each read: `n` is the count of meter readings taken in. A reading counts when it arrives, not when its value changes, so a steady load on a 1 s meter is not a slow meter.""" if n != self._n: self._n = n self._at.append(now) while self._at and self._at[0] < now - M_FIT_S: self._at.popleft() def slow(self): """The usual gap between readings is over SLOW_GAP_S.""" ts = list(self._at) if len(ts) < SLOW_ROWS: return False gaps = sorted(b - a for a, b in zip(ts, ts[1:])) return gaps[len(gaps) // 2] > SLOW_GAP_S def reset(self): self.pend = None def tuning(self, tuning, grid_w, slow): """The tuning for this cycle: `tuning`, or with the gain cut while the last move has not shown on the meter (`grid_w`).""" p = self.pend if p is None: return tuning g0, move, waited = p if not slow or (g0 - grid_w) / move >= SHOWN or waited >= WAIT_CYCLES: self.pend = None return tuning self.pend = (g0, move, waited + 1) return replace(tuning, gain=tuning.gain * WAIT_SHARE) def moved(self, grid_w, prev_w, new_w, slow): """After a cycle: start waiting if the loop moved by MOVE_W or more.""" if slow and self.pend is None and abs(new_w - prev_w) >= MOVE_W: self.pend = (float(grid_w), float(new_w - prev_w), 0)