Skip to content
Merged
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
203 changes: 130 additions & 73 deletions docs/thermal-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,139 @@

*[← back to the README](../README.md)*

## Thermal derate forecast

The Gen 3 raises alert 40 ("high temperature
detected") when its plug-handle sensor hits 65 °C, halving charge current
for the rest of the session. The handle warms along a first-order lag whose
parameters (`wallmonitor/thermal.py`) are fitted per-install from your own
recorded charging ramps (with defaults from a telemetry-verified alert-40
event), and the idle handle sits ~2 °C above ambient, so the charger doubles
as its own thermometer. Fitting is per charging **segment**, not per
session: one plug-in routinely contains several distinct draws hours
apart — the vehicle's own state-of-charge top-offs, scheduled-departure
preconditioning, or a charging schedule (common with time-of-use rates or
home batteries). The charger reports no "scheduled charging" state for any
of these (telemetry-verified: with a vehicle-side schedule armed overnight
it idles in ordinary connected states until the car draws), so the fitter
finds each segment's opening ramp wherever it occurs in the session and
lets the quality gates decide what teaches the model — no configuration or
"monitoring mode" needed. The unit of thermal analysis is the **load
window** — the stretch where current actually flows — and ambient is a
**bracket, not a point**: read at the window's start from the flat idle
stretch before it (or, when a segment starts on a still-warm handle —
stop/resume, a post-derate resume — from the previous charge's
**cool-down tail** extrapolated to its asymptote at the install's fitted
τ, so exactly the hardest-working segments aren't the ones excluded from
degradation tracking), and read again at the window's end from the
charge's own cool-down tail. When both ends read, the fit is de-trended
against the ambient ramp between them: a garage that warms 3 °C during an
afternoon charge (or cools overnight) is measured and removed instead of
masquerading as connector resistance — a single start-of-window ambient
silently assumes the weather held still for the whole charge, and a
baseline recorded in one season would otherwise bias every comparison
that follows. Fits that could only read one end fall back to the point
ambient and say so. The Live page forecasts: during charging, whether
and when the current session will derate (from the handle's live
trajectory); when idle, the estimated ambient and whether a full-rate
charge started now would trip. When a derate is coming it also suggests
the highest vehicle charge-current cap that stays under the limit —
a steady capped rate charges faster than full rate folding back to 50%.
During cool-down — after a current cut or a derate — the forecast reports
the true lower equilibrium the handle is settling toward ("recovering",
not "tripping"). When a mid-session current change resets the live
trajectory window, or sessions run back-to-back with no idle gap to read
ambient from, the forecast bridges with ambient inferred from the newest
steady run still in the buffer instead of going dark.
**Field-validated live:** steering the vehicle's charge
current down on the forecast's advice kept a session 0.7 °C under the
trip point, and in a deliberate full-rate test the trajectory forecast
predicted the actual alert-40 raise to within seconds. `/api/thermal`
returns the fitted model, the live forecast, every per-segment fit, and
the drift verdict.
## What the charger does

The Gen 3 raises alert 40 ("high temperature detected") when its plug-handle
sensor hits 65 °C, halving charge current for the rest of the session. The
handle warms along a first-order lag: an exponential approach to a plateau
set by ambient temperature plus a rise that depends on current. Everything
below is built on measuring that lag for *your* install and using it before
the trip happens.

## The model

- **Parameters are fitted per install** (`wallmonitor/thermal.py`) from your
own recorded charging ramps: the time constant τ and the steady-state rise
at 48 A. Defaults come from a telemetry-verified alert-40 event and are
replaced as sessions accumulate.
- **The charger is its own thermometer.** Idle, the handle sits ~1–2 °C above
ambient (a calibrated, ambient-dependent offset — see
`contrib/calibrate_idle_offset.py`), so ambient can be read without any
extra sensor. A LAN ambient sensor or the car's thermometer, when present,
take precedence — see [Ambient sensing](ambient-sensors.md).

## What gets fitted: segments, not sessions

One plug-in routinely contains several distinct draws hours apart — the
vehicle's own state-of-charge top-offs, scheduled-departure preconditioning,
or a charging schedule (common with time-of-use rates or home batteries). The
charger reports no "scheduled charging" state for any of these
(telemetry-verified: with a vehicle-side schedule armed overnight it idles in
ordinary connected states until the car draws).

So the fitter works per **charging segment**: it finds each segment's opening
ramp wherever it occurs in the session and lets the quality gates decide
what teaches the model. No configuration or "monitoring mode" is needed.

The unit of thermal analysis is the **load window** — the stretch where
current actually flows.

## Ambient is a bracket, not a point

A single start-of-window ambient silently assumes the weather held still for
the whole charge, and a baseline recorded in one season would then bias
every comparison that follows. Instead each window's ambient is read at
both ends:

- **At the start**, from the flat idle stretch before the window. When a
segment starts on a still-warm handle — a stop/resume, a post-derate
resume — there is no idle stretch, so ambient comes from the previous
charge's **cool-down tail**, extrapolated to its asymptote at the install's
fitted τ. That keeps exactly the hardest-working segments from being the
ones excluded from degradation tracking.
- **At the end**, from the charge's own cool-down tail.

When both ends read, the fit is **de-trended** against the ambient ramp
between them: a garage that warms 3 °C during an afternoon charge (or cools
overnight) is measured and removed instead of masquerading as connector
resistance. Fits that could only read one end fall back to the point
ambient and say so (`ambient_source` on every fit in `/api/thermal`).

## The live forecast

The Live page answers a different question depending on state:

- **While charging** — whether and when the current session will derate,
from the handle's live trajectory.
- **While idle** — the estimated ambient, and whether a full-rate charge
started now would trip.
- **When a derate is coming** — the highest vehicle charge-current cap that
stays under the limit. A steady capped rate charges faster than full rate
folding back to 50 %; the optional [amp controller](amp-control.md) can
apply that cap automatically.
- **During cool-down** — after a current cut or a derate, the forecast
reports the true lower equilibrium the handle is settling toward
("recovering", not "tripping").

When a mid-session current change resets the live trajectory window, or
sessions run back-to-back with no idle gap to read ambient from, the
forecast bridges with ambient inferred from the newest steady run still in
the buffer instead of going dark.

Every 30 s tick is recorded, so the session page can show in hindsight what
was predicted against what the handle did. The line is labelled *predicted
plateau (if this current holds)* for a reason: it is the asymptote at the
present current, not where a six-minute top-off will stop — see the faint
model-only ticks before trajectory data exists.

**Field-validated live:** steering the vehicle's charge current down on the
forecast's advice kept a session 0.7 °C under the trip point, and in a
deliberate full-rate test the trajectory forecast predicted the actual
alert-40 raise to within seconds.

`/api/thermal` returns the fitted model, the live forecast, every
per-segment fit, and the drift verdict.

## Degradation watch

The same per-segment fits feed a trend: rising heat at
unchanged current means added resistance (loose lug, degrading contact),
so when recent segments' fitted rise climbs past the baseline the poller
raises a monitor alert and the Alerts page charts the fitted-rise trend.
The watch compares only sessions near the install's *recent* operating
current: cap the vehicle at a new amperage and the watch follows,
rather than judging forever against a current the install no longer
uses; ambient-bracketed fits are clean enough under the I² normalization
to pool in from a wider current band, so a baseline recorded at 48 A
keeps judging charges after a cap to 40 A instead of the verdict going
dark. The verdict also **carries its own uncertainty**: per-side spread
(MAD) and a small-sample Student-t ~95% confidence interval on the
delta, with a separate `confident` flag — the alert threshold is a
tripwire, and the UI and notifications distinguish "statistically
confirmed" from "a lead from a four-fit baseline". And because a
baseline is only as meaningful as the hardware behind it, a
The same per-segment fits feed a trend. Rising heat at unchanged current
means added resistance — a loose lug, a degrading contact — so when recent
segments' fitted rise climbs past the baseline, the poller raises a monitor
alert and the Alerts page charts the fitted-rise trend.

### What it compares

- **Only sessions near the install's recent operating current.** Cap the
vehicle at a new amperage and the watch follows, rather than judging
forever against a current the install no longer uses.
- **Pooled across a wider current band when the fits are clean.**
Ambient-bracketed fits are clean enough under the I² normalization to pool
in, so a baseline recorded at 48 A keeps judging charges after a cap to
40 A instead of the verdict going dark.

### How sure it is

The verdict **carries its own uncertainty**: per-side spread (MAD) and a
small-sample Student-t ~95 % confidence interval on the delta, with a
separate `confident` flag. The alert threshold is a tripwire; the UI and
notifications distinguish "statistically confirmed" from "a lead from a
four-fit baseline".

### What it compares against

A baseline is only as meaningful as the hardware behind it. The
**verified-baseline anchor** (button on the Alerts page, or
`POST /api/thermal/baseline-anchor`) excludes all fits recorded before a
hardware inspection: from then on the comparison means "vs verified
healthy", not "vs the first charges the monitor happened to see".
A **rise-vs-ambient scatter** on the same page separates the remaining
confounder the fits can't remove: ambient is subtracted per fit, so a
healthy install shows a flat cloud regardless of garage temperature — a
cloud still sloping upward with ambient exposes an environment effect
the model doesn't carry (multi-day heat soak of cable and structure in
an uninsulated garage), while an elevated-but-flat cloud is the genuine
added-resistance signature.

### The confounder the fits can't remove

A **rise-vs-ambient scatter** on the same page separates the one thing
left. Ambient is subtracted per fit, so a healthy install shows a flat cloud
regardless of garage temperature:

- a cloud **still sloping upward with ambient** exposes an environment
effect the model doesn't carry — multi-day heat soak of cable and
structure in an uninsulated garage;
- an **elevated-but-flat** cloud is the genuine added-resistance signature.
Loading