While the add-on is down the batteries are at 0 W (it sets 0 W when it stops; the ESP32 watchdog after 30 s), so the import before its first reading was the house, not the aim. Those seconds now count at gap_w(limit, the import seen first), and after a restart the restored reading's held 30 s is topped up the same way. Status row: "counted as high use". Tests take the pre-start import as the house (batteries at 0 W) and add restarts of 10/60/150 s with a restored averager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.9 KiB
SAFETY-90 - the peak guard goes on after a P1 meter gap
Status: DEV 2026-10-05, for review. Tests: test_safety90.py.
The defect. A meter gap longer than meter_max_age_s leaves seconds the
quarter average does not count (p1.QuarterAverager, on purpose: a guess must
not reach the bill record). peakguard.blindness() then called the projection
blind for the rest of the quarter, so the floor was 0 W. During a HOLD the
battery gave nothing and the quarter went over: SAFETY-86 rig case 7b, N=2,
3964.8 W against a 3450 W aim, the month peak raised; N=1, 3701 W (review).
The page still read "Keeping the grid near zero". At start-up mid-quarter the
same rule left the start-up quarter blind, and a held battery was drawn as last
resort (SAFETY-91 test, O2).
The fix (peakguard.fill_gaps(), peakguard.gap_w(),
Controller._fill_peak_gaps()).
- Once readings are back, every second of a silence longer than
meter_max_age_scounts atgap_w = max(the limit, the import just before the gap, the import just after it), where "import" is the grid plus the batteries' discharge, i.e. with the batteries at 0 W. The gap's firstmeter_max_age_s, which the averager credits at the last reading, is topped up togap_wtoo. The guard then grades the quarter as usual, so it is not blind and gives what keeps the quarter at the aim. - The bound. The count is at or above the true import as long as, during the gap, the house load (less solar) stayed at or under the higher of the loads seen just before and just after it, or under the limit, and the batteries did not start charging (during a gap they hold their last command, then go to 0 W). It never counts the gap below the limit. A load that rises above all three only inside the gap is the one case it can miss.
- Cost of the bound. It is conservative: with the house at 6500 W, a 30 s to 5 min gap lands the quarter 100-180 W under the aim (table below), paid in battery energy.
- While the gap lasts nothing changes: the reading is too old to steer by, the guard is blind (floor 0 W) and the "inputs missing" stop sets 0 W, as before.
- Before the first reading (start-up or restart mid-quarter) the
add-on was down, so the batteries were at 0 W (it sets 0 W when it stops,
the ESP32 watchdog after 30 s). Those seconds count like a gap, at
gap_w(the limit, the import seen first); after a restart the restored reading's heldmeter_max_age_sis topped up the same way. The bound is the gap's, with only the "after" side known. A load the batteries cannot make up then raises "Load too high", which is true: that quarter is going over. The page says so: the status row "Peak guard: part of this 15-minute period was not measured, counted as high use" and the home line "Part of this 15-minute period was not measured." / "Een deel van dit kwartier is niet gemeten." until the quarter ends. Review: the first version counted them at the aim, with "nothing is known" as the reason. The first reading does show the load, and with the batteries at 0 W the import was that load, not the aim: a 60 s restart during a 6500 W HOLD landed 3653 W against 3450 W. - The bill record (
quarter_blocks) is unchanged: it still banks only what was measured. The estimate steers the guard and is never banked. - The action log gets one
PEAK_GUARD_GAProw per gap, with the seconds and the watts it was counted at. - No option. The threshold is
meter_max_age_s, the limit and the aim arepeak_cap_wandpeak_margin_w.
Before -> after (test_safety90.py, quarter as the meter really averages
it, gap starting 300 s in; 10 s battery lag; aim 3450 W):
| case | before | after |
|---|---|---|
| N=1 HOLD 6500 W, gap 30 s / 60 s | 3456.6 / 5329.2 | 3346.2 / 3272.2 |
| N=1 HOLD 5500 W, gap 5 min | 4717.9 | 3330.6 |
| N=2 HOLD, B able, 6500 W, gap 30 s / 60 s | 3456.6 / 3865.6 | 3346.3 / 3272.7 |
| N=2 HOLD, B able, 5500 W, gap 5 min | 3745.4 | 3288.9 |
| N=2 HOLD, B held, 5500 W, gap 60 s | 3254.9, B drawn 2500 W | 3335.4, B 0 W |
| gap across the edge, next quarter, N=1 / N=2 | 4100.0 / 3621.2 | 3446.3 / 3382.2 |
| house 3000 -> 6500 W inside a 60 s gap, N=1 | 5255.6 | 3368.3 |
| house 6500 -> 2000 W late in a 5 min gap, N=1 | 3629.2 | 3145.1 |
| start-up 450 s in, N=1, house 5000 W (batteries at 0 W before it) | 5000.0 | 3450.0 |
| restart, down 60 s / 150 s, N=1, house 6500 W | 4039.1 / 5483.3 | 3450.0 / 3450.0 |
| meter never drops, N=1 and N=2 | rows identical |
Not changed. The late-reading edge (SAFETY-91 test, O1): a reading late by
less than meter_max_age_s is not a gap, and the seconds since it are not
counted in the projection (on any quarter, not only after a gap). Near a
quarter's end the 900/remaining gain turns that into a large negative excess
and the floor drops for a cycle or more. Counting those seconds at the held
reading would fix it, but it changes every quarter's last seconds on a
healthy meter; it is handed back as its own card.