MAINT-14: maintenance with no SoC reading: charge times out, drain holds at 0 W
checks / unit (pull_request) Successful in 35m13s
infra-pin / fresh (pull_request) Successful in 1s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-08 20:37:01 +02:00
co-authored by Claude Opus 5.5
parent 8a17711f60
commit f39e52efd3
4 changed files with 507 additions and 10 deletions
@@ -0,0 +1,70 @@
# MAINT-14 - no SoC reading in a maintenance cycle
**Status: for review.** Tests: `test_maint14.py`. No option added; the one new
number is a constant in `app/maintenance.py`.
**The fault.** `Maintenance.tick()` returned early when the SoC was `None`,
before any timeout check. (1) A charge on a pack whose SoC reading was lost
never ended: no timeout, no back-off, no notice, and the 7-day backstop of
MAINT-13 sat behind the same return. (2) A drain kept commanding `discharge_w`
with an unknown level, which breaks the TEL-08 rule that an unknown level never
allows discharge.
## Rules
1. **Charge, no SoC.** The charge timeout (`CHARGE_TIMEOUT_MIN`, gate-open
minutes) and the 7-day wall backstop still run. When one fires: if the last
SoC seen was >= target - 3 the charge counts as done (the MAINT-13 "close
enough to full" rule, HOLD follows); otherwise it fails: MAINT-13 back-off
and its one notification. If no SoC was ever seen, it fails.
2. **Drain, no SoC.** DRAIN holds at 0 W and owns the setpoint, exactly as HOLD
does. The 12 h drain timeout still runs and fails as before.
3. **SoC comes back.** Nothing is recorded; the drain continues at the drain
rate, the charge carries on, and the stall wait of MAINT-13 restarts from
what it was: it paused while the reading was missing, so a gap never counts
as "no more charge".
4. **HOLD** is unchanged (0 W).
5. **Log.** One line when a drain starts holding for this reason ("the battery
level is unknown, so the drain holds at 0 W until it is back"), and one line
when the level has been missing for `NOSOC_NOTE_MIN` = 15 min. Both are said
once per episode; the old line that repeated every tick is gone.
## Why
- **"Last seen", not "best seen".** A pack that was at 99 and then dropped to 50
before the reading went is not close to full. Only the last reading counts.
It is saved with the charge record (`charge_track.last`), and saved at once
when the reading first goes missing, so a restart mid-gap still knows it
(within the usual 5 min save interval otherwise).
- **Failure when nothing was seen.** With no level we cannot call a pack full.
The failure costs a 7-day wait and one notice; calling it done costs a
missed balance and a month-long false "complete". The first is the honest one.
- **Timeouts count the same minutes as with a reading.** Gate-open charging
minutes only; the wall backstop for a gate that never opens.
- **15 min.** A judgement. A poll glitch of a few minutes should not write a
line; a reading lost for a quarter of an hour is worth one. Raise it if the
action log shows noise.
- **0 W, not "stop the phase".** Aborting a drain on a short gap would throw
away the part already drained. Holding at 0 W loses nothing and the 12 h
timeout is the limit.
## Cost
- A drain that loses its reading stops draining, and the 12 h clock keeps
running. A gap of many hours can therefore fail a drain that was working. The
back-off then applies (7 days).
- A charge whose reading is lost early (last SoC below target - 3) fails at the
timeout, even if the pack was in fact filling. The same 7-day wait applies.
## What this does not cover
- **HOLD with no SoC** still holds 0 W and, as before, does not end while the
reading is missing: its time check sits behind the same early return. A HOLD
needs no level to end, so this is a candidate follow-up, not part of this fix.
- It does not tell the operator why the reading is missing (sensor, register,
link). It does not reuse the stale-input gate; it is the cycle's own rule.
- The charge itself is still driven by the control loop, which has its own
handling of a missing SoC (TEL-08 for the loop is a separate card).
- A restart loses at most 5 min of accounting, as in MAINT-13. The "unknown for
long" line may be said again after a restart: the episode start is in memory.
- Text added is two log lines, English only, like the other log lines.