MAINT-28: during a real peak the maintenance charge stays paused until the house is clear of the aim
infra-pin / fresh (pull_request) Successful in 2s
checks / unit (pull_request) Failing after 1h10m11s

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-12 21:51:56 +01:00
co-authored by Claude
parent 1b5f7c9ccb
commit 8cd10ee6aa
5 changed files with 278 additions and 2 deletions
@@ -0,0 +1,21 @@
# MAINT-28 - during a real peak the maintenance charge stays paused
**Status: for review.** Tests: `test_maint28.py`. No option added; the peak aim, the month peak and the MAINT-13 numbers are unchanged. MAINT-24 follow-up (rig test finding F-1, `reports/MAINT-24-test-report.md`).
## What happened (rig x10, house 6.5 kW, month aim 5.95 kW, A in maintenance CHARGE)
`peakguard.shaping_suspended()` pauses the charge while the forecast is over the aim or the guard is engaged. MAINT-24 moved the aim from 3450 W to the month aim (SAFETY-80), so the pause no longer lasts the whole peak: the guard gives, its projection drops under 5950 W and it idles ("heading for 5 6xx W, under this month's peak"), the pause ends, the floor charges A 150-460 W, the grid goes to 6.3-6.9 kW, the guard trips again. A flipped charge/give 468 times in 45 min (base 02eac3b 11). The quarters stayed at the aim; the cost was a battery working against itself and a grid that swung 5.4-6.9 kW.
## The change
`peakguard.shaping_held(held, decision, forecast, aim, house)`: the old conjunction, and once it was true the pause stays until the house (`grid + battery power`, the load without our own charge) is `MAINT_RESUME_UNDER_W` = 300 W under the aim. A guard that is engaged or a forecast over the aim still pauses at once. House unread: the forecast stands in; neither read: no pause (SAFETY-63). `ctl_cycle.py` keeps the flag in `_maint_peak_hold`.
Why the house and not the forecast: while A gives, the forecast sits just under the aim by design, so no forecast margin is both safe and reachable. The house does not move when A switches.
Why 300 W: the house wanders +-50 W; once the charge resumes it settles at (aim - house) / 2, so 300 W gives at least 150 W, three times the noise, and charging less than that is not worth a restart.
It only ever keeps the maintenance charge off. No cut-back is delayed or weakened: the guard's claim is filed as before. While the pause is held the loop works as at 02eac3b, so A also gives for the house (quarters then sit under the aim, A's energy goes to the load). A cap on that give would conflict with the guard's `lo` (`lo > hi` resolves to 0 W), so it is not tried here.
## Numbers (sim, real `cycle()`, house 3000 -> 6500 W for 45 min -> 3000 W, +-50 W, aim 5950 W, forecast = quarter average so far plus the rest at the present grid; `scratch/MAINT-28/dev/m28_repro.py`)
See the dev report for the table. Live shape (house 3000 W): no pause, A 1475 W, as MAINT-24.
## Not changed
MAINT-24's rate and its tests, N=1, the drain, modes off (digests of 02eac3b). The pause for a late or missing forecast.