HOLD's time check moves into Maintenance._hold(), used by step() and
_no_soc(). A hold that loses its SoC now ends at hold_min, writes
last_completed and logs the same COMPLETE line. The 15 min HOLD line says
the hold still ends at its normal time. Behaviour with a reading is
unchanged (differential trace against db9e231).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.7 KiB
3.7 KiB
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
- 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. - 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.
- 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".
- HOLD is unchanged (0 W).
- 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: closed by MAINT-15 (
MAINT-15-hold-ends-without-soc.md). - 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.