3.8 KiB
MAINT-24 - a started maintenance charge gets the cheap night
Status: for review. Tests: test_maint24.py (test_opt20.py's ch_ch_maint digest moved with it). No option added; the MAINT-13
timeout numbers are unchanged. Folds in SAFETY-80. SAFETY-111 stays on its card (section 3).
What happened (live 0.21.0, A, 2026-11-08..11). A sat in its maintenance
CHARGE phase for more than 75 h and stalled at 49-50 %. At night B's battery-mode
grid charge took the cheap run: A is not idle, so modetrade.chargers() never
lists it, B is the only charger, and the allocator hands the whole negative
target to B. A's own shaping then paused or shrank (the check compared the peak
forecast with cap - margin = 3450 W, while B's trade and the guard aim at this
month's peak, about 4.9 kW), and A gave power back on guard stops against a peak
that B's own charge made. The timeout (MAINT-13) counted that starved time as
charging time and ended the cycle. Report: reports/MAINT-24-investigation-report.md.
1. The maintenance charge goes first in the cheap run
modetrade.decide(maint_charging=), set in _mode_roles() when any pack is in
its maintenance CHARGE phase. It is the mirror of draining (SAFETY-78): no
grid-charge trade for the other packs, why cycle, the same Tick. The other
packs keep their solar roles, so they may still take spare solar. A refusal probe
(SAFETY-73, PROBE_W alone, OPT-20) still goes on: it places no charge on the
pack in maintenance, and test_opt20.py g7 pins it. The drain still blocks it. B's night
charge waits for the nights A needs. At N=1 the only pack is the one in
maintenance, so nothing changes.
2. The shaping aims where the guard aims (SAFETY-80)
The pause (shaping_suspended()) and the floor (maintenance_charge_floor())
get peakguard.quarter_target_w(cap, margin, month reading) instead of
peak_cap_w - peak_margin_w. With no banked peak above the cap that is the same
number. SAFETY-63 (no forecast: no pause, configured rate) and SAFETY-125 (late
reading: charge_room_w) are untouched. peak_aim_w() stays for the day plan.
3. SAFETY-111 is not folded in
peak_fc holds the pack's own charge, so the floor aim - peak_fc settles at
(aim - house) / 2, here 1475 W instead of 2000 W. Taking the own charge off the
forecast was tried: it lifts A to 2000 W, but it changes the N=1 charge that
test_safety119.py pins (a static 800 W forecast then no longer shapes the
charge), so it is a change of its own. 1475 W fills a 10 kWh pack inside the
night (sim below). It stays on its card.
4. A gives nothing while no real peak exists
With 1 and 2 no grid charge of B makes a peak against the lower aim, and the pause comes only when the forecast is over the month aim, so A is not made the only battery that gives while B charges. A guard stop on a real peak still lands on A (SAFETY-07 / MAINT-02, unchanged).
Numbers (sim, house 3000 W, cap 3500, margin 50, month peak 6000 W)
base 02eac3b |
MAINT-24 | |
|---|---|---|
| A charge | 0 W (B charges 2500 W) | 1475 W = (5950 - 3000) / 2 |
| B grid trade | yes, quarters 5500 W | none, why cycle |
| quarter peak | 5500 W | 4475 W (aim 5950 W) |
| A at house 3800 W | 0 W | 1075 W, quarters 4876 W |
Whole night (sim, tools-style replay, N=2, cheap 21:00-06:00, A from 10 %, B
from 30 %, 9 h): base A takes 1.41 kWh (10 -> 23 %), B takes 6.84 kWh by grid
trade; MAINT-24 A takes 9.47 kWh and is full (100 %) inside the night, B takes
no grid charge. Worst 15-minute average 5500 W on base, 4475 W now, A never
gives.
Not changed
The MAINT-13 timeout, the peak aim itself, the month peak, P_MAINT_SHAPE and
P_ARBITRAGE. The Home card for B does not say "the other battery is cycling"
for a CHARGE phase (other_cycle reads "emptying"); that is a wording card.
MAINT-25 (the Home plan and the back-off) has its own note: MAINT-25-plan-backoff.md.