Context
#5 established that the handle-proxy ambient (ambient ≈ idle handle − offset) needs a calibrated, ambient-dependent offset rather than a constant, and was closed when idle_offset_c() landed: a linear model (1.4 °C at 30 °C, slope −0.124 °C/°C, clamped to 23–38.5 °C) fitted from 42 settled-idle segments over 8 days on one install, with contrib/calibrate_idle_offset.py to re-derive the constants by hand.
That resolved the value half of #5. Its "possible fix 1" — refit against accumulated measured-ambient data automatically — is the half that didn't ship: the constants are still hardcoded in thermal.py, so every other install starts from (and, without a sensor, stays on) numbers that describe one garage in one August. This is the largest remaining place where a single-install prior governs behaviour; #21–#23 pulled the fit gate, the drift threshold, and the τ/rise priors back to per-install, and this is the next one.
What the offset governs
Every proxy-tier fit (ambient_source pre_idle / cooldown_tail), the idle ambient tile, and the forecast's ambient whenever no sensor reports. The error is a constant bias per install, not noise — so it largely cancels in the drift watch (a difference of same-tier fits) but lands 1:1 on the derate forecast's plateau, against a SUGGEST_MARGIN_C of 2.0 °C. That is the real exposure, and it is a sensor-less user's exposure.
Proposal (one PR, one concern: the idle-offset prior becomes per-install)
- Sensor present (LAN or car) → calibrate per install, automatically. Move the estimator core from
contrib/calibrate_idle_offset.py into thermal.py (settled-idle gating, per-segment means, day-jackknifed slope). Run it at session end alongside the drift recheck; persist ref / slope / covered ambient range to settings; idle_offset_c() prefers the install's own numbers with the built-ins as fallback. /api/thermal's model object reports idle_offset_source: "built-in" | "calibrated", segment count, and the range covered. The shipped constants become the seed everyone starts from, not the calibration.
- No sensor → carry the uncertainty rather than pretend. No ground truth means no fit. Attach a ±1.5 °C proxy uncertainty to the forecast's
steady_state_se_c when ambient is proxy-sourced — the amp controller's confidence guard already consumes that field, so it turns conservative on sensor-less installs without touching the margin constant. Plus a one-line dashboard note: ambient is inferred from the idle handle using a one-install calibration; a LAN sensor (POST /api/ambient) replaces it.
- Recalibration hygiene. The comment block above
IDLE_OFFSET_* already warns that a recalibration shifts every proxy-tier fit and reads as a drift step. When the auto-calibration first lands or moves materially, record an idle_offset_calibrated event and mark it on the rise-vs-date chart so a step there carries its explanation.
Verification to bring
- The in-process calibrator must reproduce the contrib script's result on the same data (2026-08 calibration: mean 1.45 °C, 95% CI [1.13, 1.78], slope −0.124) — the proof it is the same estimator.
- Synthetic install with a true offset of 3 °C seeded against the 1.4 built-in: the calibrated path recovers ~3, and proxy-tier fits'
rise_ref_c land on the seeded truth.
- Sensor-less synthetic install: forecast
steady_state_se_c widens by the proxy term and the confidence guard's behaviour follows.
Out of scope, stated
A per-install offset for a sensor-less user is impossible without ground truth; the honest answer there is the uncertainty term and the note, not a cleverer guess. Sensor placement contamination is #3's concern and stays there.
Related: #5 (the value; closed), #3 (sensor placement), #1 (roadmap).
Context
#5 established that the handle-proxy ambient (
ambient ≈ idle handle − offset) needs a calibrated, ambient-dependent offset rather than a constant, and was closed whenidle_offset_c()landed: a linear model (1.4 °C at 30 °C, slope −0.124 °C/°C, clamped to 23–38.5 °C) fitted from 42 settled-idle segments over 8 days on one install, withcontrib/calibrate_idle_offset.pyto re-derive the constants by hand.That resolved the value half of #5. Its "possible fix 1" — refit against accumulated measured-ambient data automatically — is the half that didn't ship: the constants are still hardcoded in
thermal.py, so every other install starts from (and, without a sensor, stays on) numbers that describe one garage in one August. This is the largest remaining place where a single-install prior governs behaviour; #21–#23 pulled the fit gate, the drift threshold, and the τ/rise priors back to per-install, and this is the next one.What the offset governs
Every proxy-tier fit (
ambient_sourcepre_idle/cooldown_tail), the idle ambient tile, and the forecast's ambient whenever no sensor reports. The error is a constant bias per install, not noise — so it largely cancels in the drift watch (a difference of same-tier fits) but lands 1:1 on the derate forecast's plateau, against aSUGGEST_MARGIN_Cof 2.0 °C. That is the real exposure, and it is a sensor-less user's exposure.Proposal (one PR, one concern: the idle-offset prior becomes per-install)
contrib/calibrate_idle_offset.pyintothermal.py(settled-idle gating, per-segment means, day-jackknifed slope). Run it at session end alongside the drift recheck; persist ref / slope / covered ambient range tosettings;idle_offset_c()prefers the install's own numbers with the built-ins as fallback./api/thermal's model object reportsidle_offset_source: "built-in" | "calibrated", segment count, and the range covered. The shipped constants become the seed everyone starts from, not the calibration.steady_state_se_cwhen ambient is proxy-sourced — the amp controller's confidence guard already consumes that field, so it turns conservative on sensor-less installs without touching the margin constant. Plus a one-line dashboard note: ambient is inferred from the idle handle using a one-install calibration; a LAN sensor (POST /api/ambient) replaces it.IDLE_OFFSET_*already warns that a recalibration shifts every proxy-tier fit and reads as a drift step. When the auto-calibration first lands or moves materially, record anidle_offset_calibratedevent and mark it on the rise-vs-date chart so a step there carries its explanation.Verification to bring
rise_ref_cland on the seeded truth.steady_state_se_cwidens by the proxy term and the confidence guard's behaviour follows.Out of scope, stated
A per-install offset for a sensor-less user is impossible without ground truth; the honest answer there is the uncertainty term and the note, not a cleverer guess. Sensor placement contamination is #3's concern and stays there.
Related: #5 (the value; closed), #3 (sensor placement), #1 (roadmap).