Files
goodwe-addon/goodwe_controller/DECISIONS/MAINT-24-charge-first.md
T
glenn schrooyenandClaude de0790e62d
checks / unit (pull_request) Successful in 27m21s
infra-pin / fresh (pull_request) Successful in 1s
MAINT-25: the Home plan shows no cycle during the 7-day back-off
homeview.maintenance_runs() ignored backoff_until(); it drew a cycle every
night of the wait. Now no run and a plain line (EN, NL). test_maint25.py.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-12 09:57:56 +01:00

4.4 KiB

MAINT-24 - a started maintenance charge gets the cheap night

Status: for review. Also fixes MAINT-25 (last section). 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 honours the 7-day back-off

After a failed cycle wants_start() waits 7 days (MAINT-13, backoff_until()), but homeview.maintenance_runs() only asked due(), so the plan drew a cycle at the start hour every night of the wait. During the back-off it now draws no run for that battery and shows one line instead: "Battery A: the monthly cycle waits until 19 November." (Dutch: "... wacht tot 19 november."). A cycle completed after the failure ends it, as in backoff_until(). The other battery is planned as before. Tests: test_maint25.py (N=1 and N=2, 1 day, 7 days, completion before and after the failure, Dutch).