Compare commits
3
Commits
MAINT-21
...
SAFETY-136
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9c119bcf | ||
|
|
c50e2279dd | ||
|
|
8508ac33f0 |
@@ -71,7 +71,8 @@ The fits run all the time; the pull request has the counts.
|
||||
**What it costs.**
|
||||
- Found 36-68 s after a lag starts at 1-2 s meters; at a 5 s meter 30 s late
|
||||
is found within 100 s after it starts (88 s measured) (the loop first moves 100 W after 60 s,
|
||||
SAFETY-129) with the export above -200 W, and 10 s late is not found. Until
|
||||
SAFETY-129) with the export above -200 W, and 10 s late is not found (SAFETY-136 calms
|
||||
the loop at a slow meter instead). Until
|
||||
then control is as before. In a quiet house the "readings late" row stays
|
||||
about 2 minutes longer after the lag ends (126 s against 32 s on base); no
|
||||
harm measured, and it clears as early as on base when the load changes.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# SAFETY-136 - a slow meter gets a gentler loop until the last move shows
|
||||
|
||||
**Status: for review.** Code: `app/settle.py`, a call in `Controller.cycle()`
|
||||
(`app/ctl_cycle.py`). Tests: `test_safety136.py`. No option, no text change.
|
||||
|
||||
**Problem.** On a 5 s meter, Home Assistant readings 10-15 s late are not
|
||||
found (SAFETY-129): they look like a slow battery (R2-1), and a hold does not
|
||||
help (a perfect one cut the export of the oven by 7 %, SAFETY-129 review).
|
||||
With moving loads the loop answers the same stale error at each cycle and
|
||||
overshoots: an oven, a kettle and hob, the owner's day or a step export 3-10
|
||||
times what they do with no lag. The fix is a calmer loop, not a detector.
|
||||
|
||||
**Decision.** After a move of 150 W or more, a loop on a slow meter runs at half
|
||||
its gain until the meter has fallen by half of that move, or for 2 cycles.
|
||||
Slow is judged from the readings that arrive: the usual gap
|
||||
between them, over the last 60 s and at least 5 readings, is over 3.75 s (the
|
||||
limit of SAFETY-128's fast meter fit). It counts arrivals, not changes of
|
||||
value: on the recorded day at 1-2 s the value-based test (`MeterFit.slow()`)
|
||||
called 24 % of the cycles slow, because the recorded house repeats a value
|
||||
for 5 s or more, and the commands then differed from base. So there is no
|
||||
option. At a meter with a usual gap of 3.75 s or less (1-3 s) it is false, the tuning is never touched and the
|
||||
commands are those of the tree before. A meter that Home Assistant passes on
|
||||
only when its value changes looks as slow as its changes are far apart, as it
|
||||
does to SAFETY-128's fits: a 1-2 s meter with a value that holds for 5 s or
|
||||
more is then treated as slow, and the loop is gentler for it (the cost below).
|
||||
With no lag the meter shows a move at the next reading, so the wait ends at
|
||||
once.
|
||||
|
||||
**Candidates** (SAFETY-129 sim, real `cycle()`, 5 s meter, sum over oven,
|
||||
kettle and hob, owner shape over the oven and over the heat pump, step down,
|
||||
step up, flat; export Wh, lag 0 / 10 s / 15 s; base 64 / 340 / 363):
|
||||
|
||||
| Candidate, only at a slow meter | 0 s | 10 s | 15 s | Import Wh, 0 / 10 / 15 s |
|
||||
|---|---|---|---|---|
|
||||
| gain 0.4 | 97 | 201 | 299 | 97 / 224 / 301 |
|
||||
| gain 0.3 | 128 | 185 | 322 | 132 / 232 / 293 |
|
||||
| step limit 500 W | 118 | 285 | 293 | 110 / 325 / 324 |
|
||||
| cut fast, raise slowly (0.6 / 0.3) | 55 | 140 | 226 | 140 / 332 / 345 |
|
||||
| ignore 1 cycle after a move | 93 | 216 | 325 | 90 / 240 / 291 |
|
||||
| ignore 2 cycles after a move, unless the meter showed half | 69 | 206 | 277 | 62 / 240 / 354 |
|
||||
| **half gain for 2 cycles after a move, unless the meter showed half** | **67** | **189** | **296** | **62 / 223 / 288** |
|
||||
|
||||
A lower gain for good, a step limit and a fixed ignore all make a 5 s meter
|
||||
with no lag worse (export +30-110 Wh, quarter peak up to 150 W): that is the
|
||||
common case. Cutting fast and raising slowly moves export to import and peak.
|
||||
Waiting only while the last move has not shown costs nothing with no lag.
|
||||
Ignoring the meter (the 2-cycle line) was worse than half gain where the plant
|
||||
is slow, not the readings late: a 12 s dead time with one inverter went from
|
||||
250 to 841 Wh import and a quarter peak of 206 to 1 509 W, so the loop keeps
|
||||
following at half gain.
|
||||
|
||||
**What it gives** (5 s meter; the pull request and the report have each
|
||||
scenario). Export summed over the seven houses: 10 s late 340 -> 189 Wh
|
||||
(-44 %, import 363 -> 223), 15 s late 363 -> 296 Wh (-19 %, import 281 ->
|
||||
288), together -31 %. At 15 s the oven and the owner's oven are not better
|
||||
(92 / 39 Wh against 91 / 37), the kettle and the steps are. No lag: export
|
||||
64.4 -> 66.5 Wh (kettle +2), import and peak the same.
|
||||
|
||||
**What it costs.** The loop follows a real step more slowly: half gain for up
|
||||
to two cycles after each move of 150 W or more. Of the 136 sim runs (every
|
||||
SAFETY-129 review scenario and its 15 harm runs), 56 at 4-10 s meters differ
|
||||
from base. Together their export is 10 363 -> 9 559 Wh (-8 %) and import
|
||||
8 389 -> 7 835 Wh (-7 %). Only 5 export more than 1 Wh and 14 import more:
|
||||
export by at most 11.5 Wh (a 10 s battery lag, +10 %), import by at most
|
||||
17 Wh (a 12 s dead time, one inverter, +7 %), and the quarter average by at
|
||||
most 35 W, except two import quarters: +73 W (10 s meter, 10 s dead time,
|
||||
export -59 Wh) and +40 W (`dead10_p6`, -6 to 34 W, export -68 Wh). Slow
|
||||
batteries with a dead time of 8-10 s export 5-39 % less; a 12 s dead time is
|
||||
the same. `READINGS_LATE` holds are the same seconds in every run.
|
||||
|
||||
**Live, a 1-2 s meter can be judged slow.** Through Home Assistant on change,
|
||||
a value that holds for 4-10 s makes a 1 s meter slow. In 20 rig scenarios the
|
||||
commands were those of base. On the recorded day (1 s, N=1, 1 day, repeats
|
||||
dropped; its values hold about 5 s) it was slow in 33 % of cycles, half gain
|
||||
in 83 cycles, 15 422 commands in both trees, 4 016 with another value (at
|
||||
most 190 W apart), cost 0.61 EUR in both (review of SAFETY-136).
|
||||
|
||||
**Does not change.** Meters of 1-2 s: the same commands in all 63 runs (34
|
||||
at 1 s, 29 at 2 s), and the recorded-day digests (see the report). The late
|
||||
verdict, the hold and the freeze tests. 15 s late is only partly helped, and
|
||||
30 s late is the hold's (SAFETY-129).
|
||||
@@ -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
|
||||
|
||||
@@ -197,6 +197,7 @@ class InputsMixin:
|
||||
# correlates 0.998 with battery power and 0.09 with the real meter
|
||||
# (a controller would regulate against its own output).
|
||||
self.grid = self.p1.net_w
|
||||
self.settle.arrive(now_mono, self.p1.samples) # SAFETY-136
|
||||
else:
|
||||
self.grid = await self.hass.number(o.get("meter_entity", ""),
|
||||
bool(o.get("meter_invert")))
|
||||
|
||||
@@ -63,6 +63,7 @@ from .clamp import (
|
||||
)
|
||||
from . import freshness
|
||||
from . import lateness
|
||||
from .settle import Settle
|
||||
from . import dayview, homeview
|
||||
from .daycurve import SolarToday
|
||||
from . import handback
|
||||
@@ -741,6 +742,7 @@ class Controller(InputsMixin, WritesMixin, TradeMixin, CycleMixin, AlarmsMixin,
|
||||
self._arb_action = None
|
||||
self.inputs_bad_since = None
|
||||
self.answer = lateness.Answer() # SAFETY-128
|
||||
self.settle = Settle() # SAFETY-136
|
||||
self._late_logged = self._late_zero = False
|
||||
# SAFETY-03: what the loop's OWN bounds did last cycle, `(engaged,
|
||||
# note, over_rating)` (SAFETY-40 splits the last out). Site-level: the
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""SAFETY-136: on a slow meter, follow the meter gently until the last move shows.
|
||||
|
||||
A meter whose usual gap between readings is over 3.75 s (a 4-5 s meter), 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 with a usual gap of 3.75 s or less, 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)
|
||||
@@ -38,6 +38,7 @@ logging.disable(logging.WARNING)
|
||||
import safety128_rig as R # noqa: E402
|
||||
import app.main as app_main # noqa: E402
|
||||
from app import lateness # noqa: E402
|
||||
from app import settle # noqa: E402
|
||||
|
||||
fails = []
|
||||
|
||||
@@ -58,6 +59,17 @@ def no_gate(fn):
|
||||
app_main.Controller._readings_late = real
|
||||
|
||||
|
||||
def no_settle(fn):
|
||||
"""`fn()` without SAFETY-136's gentler loop: the tree this card was written
|
||||
against (with it the same run exports -645 W, not -1 325 W)."""
|
||||
real = settle.Settle.moved
|
||||
settle.Settle.moved = lambda self, *a, **k: None
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
settle.Settle.moved = real
|
||||
|
||||
|
||||
def run_late(**kw):
|
||||
"""Run the rig; (rig, seconds the verdict stood, first second it stood)."""
|
||||
dur = kw.pop("dur")
|
||||
@@ -91,7 +103,7 @@ for units, house in ((AB, 1800.0), (A, 1500.0)):
|
||||
if len(units) == 2:
|
||||
old = R.Rig(house=house, lag_s=30.0, lag_on=ON, lag_off=ON + 300.0,
|
||||
units=units, p1_s=5.0)
|
||||
no_gate(lambda: old.run(ON + 300.0))
|
||||
no_settle(lambda: no_gate(lambda: old.run(ON + 300.0)))
|
||||
check(f"{n}: without the gate the export goes below -1 kW",
|
||||
grid_min(old.rows, ON, ON + 300.0) < -1000.0)
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
"""Runnable check for SAFETY-136: a calmer loop on a slow meter.
|
||||
`python3 test_safety136.py`
|
||||
|
||||
With a 5 s meter and Home Assistant readings 10-15 s late the loop answers the
|
||||
same error again at each cycle and overshoots: with an oven, a kettle and hob,
|
||||
or a step the export was 3-10 times that of a meter with no lag. After a move
|
||||
of 150 W or more `app/settle.py` now runs the loop at half its gain (at most
|
||||
2 cycles) until the meter shows half of it. It is off for a meter under 4 s.
|
||||
DECISIONS/SAFETY-136-slow-meter-settle.md.
|
||||
|
||||
The REAL Controller on TEST-23's plant (tools/safety128_rig.py). "Off" is the
|
||||
same run with `Settle.moved` doing nothing: the tree before this card.
|
||||
|
||||
1. `Settle` alone.
|
||||
2. 5 s meter, readings 10 s late: oven, kettle and hob, a step down and a step
|
||||
up export clearly less than with it off.
|
||||
3. 5 s meter, no lag: the same houses are no worse than a small margin.
|
||||
4. 1 s and 2 s meters: the commands are those of the run with it off, and the
|
||||
loop never waits.
|
||||
5. The slow meter is told by its observed interval, at 4 s and 5 s.
|
||||
|
||||
Mutations (MUTATION.md, purged copies) are listed in the pull request.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
sys.path.insert(0, os.path.join(HERE, "tools"))
|
||||
logging.disable(logging.WARNING)
|
||||
|
||||
import safety128_rig as R # noqa: E402
|
||||
from app.control import Tuning # noqa: E402
|
||||
from named_absence import nth, present # noqa: E402
|
||||
from app import settle # noqa: E402
|
||||
from app.lateness import MeterFit # noqa: E402
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond):
|
||||
print(f" {'ok ' if cond else 'FAIL'} {name}")
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
def off(fn):
|
||||
"""`fn()` with the settle wait off: the tree before this card."""
|
||||
real = settle.Settle.moved
|
||||
settle.Settle.moved = lambda self, *a, **k: None
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
settle.Settle.moved = real
|
||||
|
||||
|
||||
def oven(t):
|
||||
return 1800.0 + (2000.0 if (t >= 300.0 and (t - 300.0) % 105 < 45) else 0.0)
|
||||
|
||||
|
||||
def kettle(t):
|
||||
w = 600.0
|
||||
if t % 400 < 120:
|
||||
w += 2200
|
||||
if t >= 900 and t % 90 < 30:
|
||||
w += 1500
|
||||
return w
|
||||
|
||||
|
||||
def step(t):
|
||||
return 1800.0 if t < 320.0 else 300.0
|
||||
|
||||
|
||||
def stepup(t):
|
||||
return 300.0 if t < 320.0 else 2500.0
|
||||
|
||||
|
||||
# name: (house, noise, run length, lag end)
|
||||
HOUSES = {"oven": (oven, 80.0, 900.0, 900.0),
|
||||
"kettle": (kettle, 80.0, 1500.0, 1500.0),
|
||||
"step": (step, 50.0, 700.0, 600.0),
|
||||
"stepup": (stepup, 50.0, 700.0, 600.0)}
|
||||
ON = 300.0
|
||||
|
||||
|
||||
def wh(rows, t0, t1, sign):
|
||||
"""Export (sign -1) or import (sign +1) energy in Wh over [t0, t1)."""
|
||||
return sum((-min(r[1], 0.0) if sign < 0 else max(r[1], 0.0))
|
||||
for r in rows if t0 <= r[0] < t1) / 3600.0
|
||||
|
||||
|
||||
def run(name, lag, p1_s=5.0, units=("a", "b"), dur=None):
|
||||
house, noise, length, lag_off = HOUSES[name]
|
||||
r = R.Rig(house=house, noise=noise, p1_s=p1_s, units=units, lag_s=lag,
|
||||
lag_on=ON, lag_off=lag_off)
|
||||
waits = []
|
||||
r.run(dur or length + 20.0,
|
||||
lambda rig, c, t: waits.append(1) if c.settle.pend else None)
|
||||
return r, len(waits)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("1. Settle alone")
|
||||
T = Tuning()
|
||||
s = settle.Settle()
|
||||
check("no move: the tuning is untouched", s.tuning(T, 500.0, True) is T)
|
||||
s.moved(500.0, 0.0, 100.0, True)
|
||||
check("a move under 150 W is not waited for", s.pend is None)
|
||||
s.moved(500.0, 0.0, 600.0, False)
|
||||
check("a meter that is not slow is not waited for", s.pend is None)
|
||||
s.moved(500.0, 0.0, 600.0, True)
|
||||
check("a move of 600 W is waited for: half the gain",
|
||||
s.tuning(T, 500.0, True).gain == T.gain * 0.5)
|
||||
check("the meter fell 200 W of 600 W: still half the gain",
|
||||
s.tuning(T, 300.0, True).gain == T.gain * 0.5)
|
||||
check("two cycles waited: the third has the whole gain",
|
||||
s.tuning(T, 300.0, True) is T and s.pend is None)
|
||||
s.moved(500.0, 0.0, 600.0, True)
|
||||
check("the meter went the other way by half the move: the wait stands",
|
||||
s.tuning(T, 800.0, True).gain == T.gain * 0.5)
|
||||
s.reset()
|
||||
s.moved(500.0, 0.0, -600.0, True)
|
||||
check("a move down, the meter rose half the move: the whole gain",
|
||||
s.tuning(T, 800.0, True) is T)
|
||||
s.reset()
|
||||
s.moved(500.0, 0.0, 600.0, True)
|
||||
check("the meter fell half the move: the whole gain at once",
|
||||
s.tuning(T, 200.0, True) is T and s.pend is None)
|
||||
s.moved(500.0, 0.0, -600.0, True)
|
||||
check("a move down is waited for too",
|
||||
s.tuning(T, 500.0, True).gain == T.gain * 0.5)
|
||||
s.reset()
|
||||
check("reset ends the wait", s.pend is None)
|
||||
s.moved(500.0, 0.0, 600.0, True)
|
||||
check("a meter that turns out not to be slow ends the wait",
|
||||
s.tuning(T, 500.0, False) is T and s.pend is None)
|
||||
check("the numbers are pinned",
|
||||
(settle.MOVE_W, settle.SHOWN, settle.WAIT_SHARE, settle.WAIT_CYCLES)
|
||||
== (150.0, 0.5, 0.5, 2))
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n2. 5 s meter, readings 10 s late")
|
||||
tot_on = tot_off = 0.0
|
||||
for name, (_h, _n, length, lag_off) in HOUSES.items():
|
||||
a, waited = run(name, 10.0)
|
||||
b, _w = off(lambda: run(name, 10.0))
|
||||
ea, eb = wh(a.rows, ON, lag_off, -1), wh(b.rows, ON, lag_off, -1)
|
||||
tot_on, tot_off = tot_on + ea, tot_off + eb
|
||||
print(f" {name}: export {ea:.1f} Wh, off {eb:.1f} Wh, waited {waited} s")
|
||||
check(f"{name}: waits at some point", waited > 0)
|
||||
check(f"{name}: exports at most 75 % of the run with it off",
|
||||
ea <= 0.75 * eb)
|
||||
print(f" sum: {tot_on:.1f} Wh, off {tot_off:.1f} Wh")
|
||||
check("the four together export at most 65 % of the run with it off",
|
||||
tot_on <= 0.65 * tot_off)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n2b. Another writer overrides the loop: the wait ends")
|
||||
halved = []
|
||||
real_tuning = settle.Settle.tuning
|
||||
|
||||
|
||||
def watch_tuning(self, tuning, grid_w, slow):
|
||||
r = real_tuning(self, tuning, grid_w, slow)
|
||||
halved.append(r is not tuning)
|
||||
return r
|
||||
|
||||
|
||||
settle.Settle.tuning = watch_tuning
|
||||
try:
|
||||
mark = []
|
||||
|
||||
def bump(rig, c, t):
|
||||
# right after a cycle that started a wait, another writer moves the
|
||||
# command, so the next cycle sees `target != loop_w`
|
||||
if not mark and c.settle.pend and t > 330.0:
|
||||
mark.append(len(halved))
|
||||
c.target = c.target + 300.0
|
||||
|
||||
r = R.Rig(house=oven, noise=80.0, p1_s=5.0, lag_s=10.0, lag_on=ON,
|
||||
lag_off=900.0)
|
||||
r.run(500.0, bump)
|
||||
finally:
|
||||
settle.Settle.tuning = real_tuning
|
||||
nxt = nth(halved, mark[0] if mark else 10 ** 6)
|
||||
check("the cycle after an override has the whole gain",
|
||||
bool(mark) and present(nxt) and nxt is False)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n3. 5 s meter, no lag")
|
||||
for name in ("oven", "kettle", "step", "stepup"):
|
||||
_h, _n, length, lag_off = HOUSES[name]
|
||||
a, _w = run(name, 0.0)
|
||||
b, _w = off(lambda: run(name, 0.0))
|
||||
ea, eb = wh(a.rows, ON, lag_off, -1), wh(b.rows, ON, lag_off, -1)
|
||||
ia, ib = wh(a.rows, ON, lag_off, 1), wh(b.rows, ON, lag_off, 1)
|
||||
print(f" {name}: export {ea:.1f} Wh (off {eb:.1f}), import {ia:.1f} Wh "
|
||||
f"(off {ib:.1f})")
|
||||
check(f"{name}: export at most 6 Wh over the run with it off",
|
||||
ea <= eb + 6.0)
|
||||
check(f"{name}: import at most 1 Wh over the run with it off",
|
||||
ia <= ib + 1.0)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n4. 1 s and 2 s meters")
|
||||
for p1 in (1.0, 2.0):
|
||||
for lag in (0.0, 10.0):
|
||||
for units in (("a", "b"), ("a",)):
|
||||
a, waited = run("oven", lag, p1_s=p1, units=units, dur=700.0)
|
||||
b, _w = off(lambda: run("oven", lag, p1_s=p1, units=units,
|
||||
dur=700.0))
|
||||
same = ([(r[0], r[1], r[2]) for r in a.rows]
|
||||
== [(r[0], r[1], r[2]) for r in b.rows])
|
||||
check(f"{p1:.0f} s meter, {lag:.0f} s late, N={len(units)}: "
|
||||
f"same commands, never waits", same and waited == 0)
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("\n5. The slow meter is told by its observed interval")
|
||||
s = settle.Settle()
|
||||
for k in range(30):
|
||||
s.arrive(float(k), k)
|
||||
check("a reading each second is not a slow meter", not s.slow())
|
||||
s = settle.Settle()
|
||||
for k in range(30):
|
||||
s.arrive(5.0 * k, k)
|
||||
check("a reading every 5 s is a slow meter", s.slow())
|
||||
s = settle.Settle()
|
||||
for k in range(4):
|
||||
s.arrive(5.0 * k, k)
|
||||
check("fewer than 5 readings in a minute tell nothing", not s.slow())
|
||||
s = settle.Settle()
|
||||
for k in range(30):
|
||||
s.arrive(5.0 * k, 0 if k else 1)
|
||||
check("the same count again is no new reading", not s.slow())
|
||||
for p1, expect in ((1.0, False), (2.0, False), (3.0, False), (4.0, True),
|
||||
(5.0, True), (10.0, True)):
|
||||
r = R.Rig(house=1800.0, p1_s=p1)
|
||||
seen = []
|
||||
r.run(120.0, lambda rig, c, t: seen.append(c.settle.slow()))
|
||||
last = nth(seen, -1)
|
||||
check(f"{p1:.0f} s meter: slow is {expect}",
|
||||
present(last) and last is expect)
|
||||
# A 1 s meter on a house that changes every 5 s repeats its value 4 times in 5.
|
||||
# Where every reading arrives (the rig, a polled meter) it is not slow; where
|
||||
# Home Assistant sends a reading only when the value changes (ha_signed) the
|
||||
# same meter is slow (T3 of the review; DECISIONS).
|
||||
s, m, h = settle.Settle(), MeterFit(), settle.Settle()
|
||||
for k in range(60):
|
||||
s.arrive(float(k), k)
|
||||
h.arrive(float(k), k // 5)
|
||||
m.add(float(k), 0.0, 1800.0 + 100.0 * (k // 5))
|
||||
check("every reading arrives, value changes every 5 s: not slow (the value "
|
||||
"fit says slow)", not s.slow() and m.slow())
|
||||
check("the same meter through Home Assistant on change (a reading per "
|
||||
"change): slow", h.slow())
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print(f"{len(fails)} FAILED: " + ", ".join(fails))
|
||||
sys.exit(1)
|
||||
print("all checks passed")
|
||||
Reference in New Issue
Block a user