v0.4.0 — Session quality fixes (post-PR-22 stabilization)
Stabilization release on top of v0.3.0's plug-anchored session model
Six bundled fixes surfaced during real-world validation of PR-22 (three charging cycles across two vehicles + one HA Core upgrade mid-session). All fixes apply additively to the existing PlugAnchoredSessionEngine; no session-store schema change, no migration required.
What's new
- RFID grace timer — plug-then-blip now produces correctly attributed sessions. After plug-on, the engine waits up to 5 seconds for an RFID blip before committing user attribution. Existing blip-then-plug fast path is unchanged. The previous race condition (plug-on commits
user=Unknown12 ms before the RFID blip propagates from the charger) is closed. - HA-restart preserves charging duration — when HA restarts mid-session, the pre-restart open charging window is now reconstructed as a synthetic closed window during deferred recovery, with
ended_at = recovery timeandenergy_kwhcovering the full pre-restart delta.charging_duration_sandavg_power_ware now physically plausible across restarts (previously: a Peugeot session reported 45 kW average for a 3.7 kW PHEV). - Window counter unified across log lines —
CHARGING_WINDOW_OPENandCHARGING_WINDOW_CLOSEnow both usesession.charging_window_count(the session-wide persisted counter). Previously, after a restart, a window could open aswindow=2and close aswindow=1because the engine-instance counter resets on restart. EV Charger Chargingbinary sensor honest about current charging — flips tooffwhen the active charging window closes (sub-statecharged), instead of stayingonfor the entire plug duration. Generic profile (legacySessionEngine) keeps its previousTRACKING-based behavior via automatic fallback.- New live
Charging durationsensor (sv: Laddningstid) — accumulating across charging windows, with per-window detail inextra_state_attributes(index,started_at,ended_at,duration_s,energy_kwh). Distinct fromSession duration(anslutningstid). The two diverge significantly when the car sits charged on the cable (e.g. 14:21:01 connection vs 04:45:50 charging on a typical overnight Mercedes session). - HEARTBEAT log + live UI dispatch — debug log now writes a
HEARTBEATline at a configurable cadence (default 5 min) while a session is being tracked, so users can verify the engine is alive during long stable charges. A separate UI dispatch timer (default 60 s) keeps the live duration sensors ticking smoothly in Lovelace between state changes.
Configuration
Three new options under integration settings:
rfid_grace_seconds(default 5, range 0–30 where 0 disables the grace timer)heartbeat_log_interval_min(default 5, range 0–30 where 0 disables HEARTBEAT logging)ui_dispatch_interval_s(default 60, value0disables; otherwise range 10–300)
Internal improvements
_RfidGraceStatedataclass replaces three scattered fields (cleaner lifecycle).SessionSubStateStrEnum replaces magic-string sub-state comparisons._Unsub = CALLBACK_TYPE | Nonealias replacesAnyon ~9 timer/listener handles._safe_cancel(attr)helper deduplicates 4 cancel patterns._WindowAttribute/_ChargingDurationAttributesTypedDicts type the new sensor's attribute shape.- Idempotency guard on
_async_start_sessionaborts late tasks after plug-off (FR-004). connection_sclamped tomax(0, ...)at all sites (protects against clock skew making real sessions look like micro-sessions).- HEARTBEAT line uses explicit
is Nonechecks for energy/power readings (no truthy-or masking of genuine 0.0 readings between windows).
Validation
Verified against three real-world charging cycles in May 2026:
- Mercedes GLB 350 ("Elvis"): 48 kWh overnight + 9.5 h "Färdigladdad" idle on cable — clean.
- Peugeot 3008 PHEV ("Petras bil") Day 1: 10.9 kWh, RFID race reproduced and now fixed.
- Peugeot 3008 PHEV Day 2: 8.2 kWh with HA Core upgrade mid-session — multi-window recovery path exercised in production.
535 unit tests pass (478 baseline + 57 new for v0.4.0). ruff format and ruff check clean.
Migration
None required. Session store stays at schema v1.2; no field renames; no data backfill. New options use sensible defaults — existing installations get the new behavior automatically. The previous immediate-start RFID behavior remains available by setting rfid_grace_seconds = 0.
Scope
This release affects only the goe_gemini charger profile. The generic profile keeps the legacy SessionEngine unchanged.
Known follow-up
When pre-restart sub-state was charged (charging completed, cable still in) and HA restarts followed by unplug without resuming charging, charging_duration_s becomes 0 because the window-tracker doesn't reconstruct closed windows from the persisted session.windows[] snapshot. The synthetic-window guard correctly prevents the previous inflation bug, but full preservation requires loading persisted windows into the tracker at recovery — tracked for a future release.