# MAINT-15 - the balancing hold ends on time with no SoC reading **Status: for review.** Tests: `test_maint15.py`. No option added. **The fault.** `step()` sent every tick with no SoC to `_no_soc()`, and for HOLD that returned 0 W without the time check. A hold that lost its reading never ended: `last_completed` was never written and the next due date never moved. Harmless for power (0 W), but the cycle was stuck. This was left open in MAINT-14. ## Rule 1. **HOLD ends at `hold_min` whether or not there is a reading.** One helper, `Maintenance._hold()`, holds the time check and is used by both paths. When the time is up it writes `last_completed`, goes IDLE and logs the same "cycle COMPLETE" line. Before the time is up it commands 0 W as before. 2. **Text.** The 15-min "no battery level" line in HOLD now reads "The hold still ends at its normal time." Still once per episode. ## Why HOLD is reached only after the charge counted as done. Every entry into HOLD is in `step()` or `_no_soc()` and follows a charge that reached the target, stalled near full (MAINT-13) or timed out near full (MAINT-13/14). Nothing else sets the phase to HOLD (`_enter(HOLD` has four call sites, all after a done charge). The hold is a wait for the BMS to balance; it never needed the level. So a hold that ends with no reading is a completed cycle. The "cycle COMPLETE" line is true without a reading: it names minutes of hold, not a level. ## Cost - If the level was lost during the hold, the cycle is recorded as complete without anyone seeing the final level. The charge had counted as done, so this is the same trust MAINT-14 already gives a charge that ends with no reading. ## What this does not cover - It does not check that the pack is still full at the end of the hold. - It does not say why the reading was lost. - With a reading, nothing changes. `test_maint15.py` section 7 compares the new HOLD tick by tick with a frozen copy of the release/1.0 HOLD branch (phase, setpoint, owner, events, `last_completed`) at four `hold_min` values. A wider trace of the whole cycle (800 seeded runs against db9e231) was run outside the repo; its script and log are kept under `scratch/MAINT-15/` of the dev workspace, not in this repository. - A state file written by an older version with `phase: hold` loads and ends on time; the file is not changed in format.