v0.4.1 — RFID wait model (event-driven, no timeout)
Hotfix for the v0.4.0 RFID grace timer
v0.4.0 introduced a 5-second RFID grace timer that committed user=Unknown if no blip arrived within the deadline. Real-world validation on 2026-05-24 showed the actual go-e trx propagation latency can reach 7 seconds, exceeding the timer and recreating the user=Unknown failure the grace timer was designed to prevent.
v0.4.1 replaces the timer with an event-driven wait that resolves on one of three triggers — trx arrives, power begins flowing, or plug is removed — with no timeout. The user can blip whenever they like; the session is attributed correctly regardless of how long it took.
What's new
- Plug-then-blip is now timeout-free. Plug in the cable, blip the RFID tag at any time (5 seconds, 30 seconds, 5 minutes — doesn't matter), and the session is attributed to the matched user. No
Unknownfallback unless the user actually unplugs without blipping. - Two new visible sub-states on the Status sensor for the pre-session waiting periods:
waiting_for_rfid(Swedish: "Väntar på RFID", English: "Waiting for RFID") — cable plugged in, awaiting RFID blip.waiting_for_plug(sv: "Väntar på kabel", en: "Waiting for cable") — RFID blipped, awaiting cable insertion.
- Open-access charger inference. If a charger configured for open access begins delivering power without an RFID blip, the integration records the session as
Unknownwith reasonopen_access_inferredso no energy is lost. (Existing trx=0 fast path also preserved.) - Renamed
waitingsub-state →initializing(sv: "Initierar laddning", en: "Initializing") to avoid name collision with the two new waiting states.
Internal improvements
UnknownReasonStrEnum replaces six bare-stringUNKNOWN_REASON_*constants — type-safe diagnostic reasons forUnknownsessions (trx_was_null, trx_was_zero, rfid_unmapped, rfid_inactive, rfid_type_error, open_access_inferred)._RfidWaitStatedataclass is nowfrozen=Truewith three documented invariants._async_start_sessionbody wrapped in try/except with persistent-notification on error — engine no longer fastnar in a ghostTRACKINGstate with no session ifSession()construction raises._dispatch_update()now fires on every trx-change event (not only on session start), so the newwaiting_for_plugsub-state reaches the dashboard immediately.- Race-condition decision documented in code (research §R3 first-fire-wins for trx vs. power triggers).
- IC-5 state-machine asymmetry (waiting_for_rfid ⇒ TRACKING; waiting_for_plug ⇒ IDLE) documented in
get_status_sub_statewith rationale. - All
WAITINGenum references renamed toINITIALIZING; allRFID gracecomments updated toRFID wait. - 7 PR-22/PR-23 test fixtures cleaned up (removed
CONF_RFID_GRACE_SECONDS: 0opt-outs; adapted to event-driven semantics).
Configuration changes
- Removed
rfid_grace_secondsoption (no longer needed; event-driven model has no configurable duration). - HA tolerates stale
rfid_grace_seconds: Nkeys in existingentry.options— no migration required, no warning.
Multi-agent review
Five specialized review agents flagged 25 findings (8 blocking + 9 should-fix + 8 nice-to-have); all resolved across four fixer-agent passes plus a final code-simplifier polish. Final verification confirmed PROCEED TO COMMIT.
Migration
None required.
- Session-store schema unchanged (v1.2 — PR-23 FR-029 still applies).
entry.optionskeys are tolerated on load; obsoleterfid_grace_secondskeys are silently ignored.- No new dependencies.
Test coverage
547 unit tests passing (was 535 on v0.4.0). Net change:
- +14 in new
tests/test_session_engine_rfid_wait.py(event-driven scenarios) - −7 deleted from
tests/test_session_engine_rfid_grace.py(PR-23 grace timer) - +6 in new
tests/test_status_sensor_sub_states.py(sub-state visibility) - +4 across edited files (charging-duration unavailable, race no-op, restart-mid-wait, etc.)
- Net +17 new tests; net +12 after the −7 deletion and minor consolidations.
Scope
Affects only the goe_gemini charger profile. The legacy generic profile keeps its SessionEngine (no waiting sub-state support). Migration to plug-anchored model for generic profile is explicitly out of scope.
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. PR-23's synthetic-window guard prevents the previous inflation bug (Elvis 9.5 h scenario); full preservation needs a follow-up to load persisted windows into the tracker at recovery. Carried over from PR-23 review.