Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions tests/test_wallmonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,27 @@ async def test_thermal_fits_the_current_exponent_from_a_current_spread(db):
assert all(fit["rise_c"] * (48.0 / fit["current_a"]) ** 2 > 40.0 for fit in low)


def test_steady_prefix_restarts_after_a_ramp_up_overshoot():
# 2026-09-14, first live calibration probe: the car overshot toward 48 A
# for two samples while the 32 A cap was taking effect, then held 32.5 A
# for forty minutes. The prefix ended at the overshoot, four samples in,
# and the whole window was lost.
def samples(currents):
return [
{"ts": 1000.0 + 2.0 * i, "contactor_closed": 1, "vehicle_current_a": amps, "handle_temp_c": 32.0}
for i, amps in enumerate(currents)
]

ramp = [6.1, 14.6, 23.9, 31.1, 32.6, 32.7, 32.9, 37.6, 43.3] + [32.5] * 600
prefix = thermal._steady_current_prefix(samples(ramp))
assert len(prefix) == 600 and prefix[0]["vehicle_current_a"] == 32.5
# A current change once the run has settled still ends it (a derate, a
# cap): the leniency is for the first minute only.
settled = [32.5] * 400 + [44.0] * 100
prefix = thermal._steady_current_prefix(samples(settled))
assert len(prefix) == 400 and prefix[-1]["vehicle_current_a"] == 32.5


def test_thermal_sustainable_current_follows_the_fitted_exponent():
square = thermal.ThermalParams(rise_ref_c=36.0, current_exp=2.0)
gentle = thermal.ThermalParams(rise_ref_c=36.0, current_exp=1.5)
Expand Down
18 changes: 15 additions & 3 deletions wallmonitor/thermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ def ambient_from_idle_handle(handle_c: float, model: IdleOffset = BUILTIN_IDLE_O
MIN_SPAN_TAU = 1.8
PREFIX_SPAN_TAU = 2.5
PREFIX_SPAN_MIN_S = 1800.0
# A steady run that ends within its first minute did not end; the ramp-up
# was still wobbling. Vehicles overshoot on the way to a cap: one session
# read 32.6, 32.7, 32.9, 37.6, 43.3 and then 32.5 A for forty minutes — the
# two overshoot samples being the car heading for 48 A in the seconds
# before the amp controller's probe cap took effect — and ending the run at
# the overshoot kept a four-sample window and lost the real one. Every
# probe lands mid-ramp, so every probe would have been lost the same way.
PREFIX_SETTLE_S = 60.0

# The steady-prefix band (10% of the reference current) is wide enough to
# hide a substantial current reduction: 48.6 A trimmed to 44.7 A never
Expand Down Expand Up @@ -383,7 +391,10 @@ def _steady_current_prefix(samples: list[dict], max_span_s: float = PREFIX_SPAN_
its samples at the reduced current, and a whole-session median would put
the initial full-rate ramp — the part with the thermal signal — outside
the band. Leading samples still ramping up to the plateau are skipped
rather than treated as the end of the run.
rather than treated as the end of the run, and a run that leaves the
band within PREFIX_SETTLE_S of starting is discarded and restarted
rather than ended: that is the ramp overshooting, not a steady run
ending.
"""
charging = [
sample
Expand All @@ -404,9 +415,10 @@ def _steady_current_prefix(samples: list[dict], max_span_s: float = PREFIX_SPAN_
if prefix and sample["ts"] - prefix[-1]["ts"] > SEGMENT_SPLIT_GAP_S:
break # a charging gap: the next samples belong to a later segment
if abs(sample["vehicle_current_a"] - i_ref) > band:
if prefix:
if prefix and sample["ts"] - prefix[0]["ts"] >= PREFIX_SETTLE_S:
break # the steady run ended (derate or charge stop)
continue # still ramping up to the plateau
prefix = [] # still ramping up, or the ramp overshot: start over
continue
prefix.append(sample)
if sample["ts"] - prefix[0]["ts"] > max_span_s: # the ramp lives in the first few tau
break
Expand Down
Loading