Smooth battery percent readings before computing drop-rate steps
A 1M-ohm divider (way over the ~10k source impedance the ESP32 ADC's sample-and-hold expects) doesn't always misfire in isolation -- short bursts of a few consecutive bad readings, and multi-reading drifts, both slip past the existing step-level MAD outlier rejection since the steps between two bad readings in the same burst look ordinary. Add a Hampel-filter smoothing pass (local-neighborhood MAD, same statistical approach as the existing outlier rejection) ahead of it.
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
"""_reject_outlier_drops -- the outlier-rejection pass in the battery
|
||||
remaining-time estimate (see routers/common.py's battery_estimate_s).
|
||||
Pure function, no DB/HTTP -- (recency_weight, drop_pct) pairs in,
|
||||
filtered pairs out."""
|
||||
"""_reject_outlier_drops and _smooth_percents -- the two outlier-rejection
|
||||
passes in the battery remaining-time estimate (see routers/common.py's
|
||||
battery_estimate_s). Pure functions, no DB/HTTP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.routers.common import _reject_outlier_drops
|
||||
from app.routers.common import _reject_outlier_drops, _smooth_percents
|
||||
|
||||
|
||||
def _steps(drops: list[float]) -> list[tuple[int, float]]:
|
||||
@@ -69,3 +68,37 @@ def test_never_filters_down_to_nothing():
|
||||
steps = _steps([1, 1, 1, 1, 10, 10, 10, 10])
|
||||
kept = _reject_outlier_drops(steps)
|
||||
assert len(kept) > 0
|
||||
|
||||
|
||||
def test_smooth_corrects_isolated_spike():
|
||||
percents = [70, 70, 70, 70, 70, 90, 70, 70, 70, 70, 70]
|
||||
smoothed = _smooth_percents(percents)
|
||||
assert smoothed[5] == 70
|
||||
assert smoothed[:5] == percents[:5]
|
||||
assert smoothed[6:] == percents[6:]
|
||||
|
||||
|
||||
def test_smooth_corrects_short_burst():
|
||||
"""The shape seen in production: several consecutive corrupted
|
||||
reports (a 1M-ohm divider glitching for a few reports in a row, not
|
||||
just one) spliced into an otherwise flat run. A step computed
|
||||
between two of these looks like an ordinary small change, which is
|
||||
exactly why _reject_outlier_drops alone can't catch this shape."""
|
||||
percents = [53, 53, 53, 53, 41, 40, 40, 42, 53, 53, 53, 53]
|
||||
smoothed = _smooth_percents(percents)
|
||||
assert smoothed[4:8] == [53, 53, 53, 53]
|
||||
assert smoothed[:4] == percents[:4]
|
||||
assert smoothed[8:] == percents[8:]
|
||||
|
||||
|
||||
def test_smooth_leaves_gradual_legitimate_trend_alone():
|
||||
"""A slow, steady climb (recharge) or decline spread over many
|
||||
reports is a real trend, not a local glitch -- each reading is close
|
||||
to its own neighborhood's median, so nothing should be flagged."""
|
||||
percents = list(range(80, 60, -2)) # 80, 78, 76, ... steady discharge
|
||||
assert _smooth_percents(percents) == percents
|
||||
|
||||
|
||||
def test_smooth_identical_readings_untouched():
|
||||
percents = [50] * 12
|
||||
assert _smooth_percents(percents) == percents
|
||||
|
||||
Reference in New Issue
Block a user