fix(vehicle): make BLE pairing survive lost whitelist replies#62
Merged
Conversation
added 4 commits
July 10, 2026 17:50
The whitelist-operation success is a single VCSEC frame; when the BLE session cycles while the user walks to the vehicle to approve, that frame can land on a dead session and be lost, hanging the plain one-shot wait (observed on HA/macOS CoreBluetooth, reconnecting every ~100s). pair() now keeps the reply as the fast path but, on a lost reply, polls whether our public key became whitelisted - a VCSEC handshake with our own key succeeds only once it is, and faults NotOnWhitelistFault until then. The whitelist op is written exactly once (never re-sent, which would re-prompt the user), transport failures mid-poll are treated as not-yet so polling survives reconnects, and an overall deadline raises BluetoothTimeout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Make VehicleBluetooth.pair() reliable when the one-shot whitelist-operation success reply is lost to a BLE session cycle. Captain repro on HA/macOS CoreBluetooth: the car initiated pairing (user approved on vehicle) but HA never recognized completion because the BLE session reconnected (~every 100s) while pair() waited, and the whitelist success reply (a one-shot VCSEC frame) landed on a dead session. Design (captain-approved): after writing the whitelist op, confirm completion either by the fast-path one-shot reply OR by verify-by-state polling every poll_interval seconds. The chosen probe is a VCSEC handshake with our own public key: it succeeds only once the key is whitelisted and faults NotOnWhitelistFault until then, directly answering 'is our key now effective on the car'. Key decisions/constraints: the whitelist op is written EXACTLY ONCE and never re-sent (a re-send would re-prompt the user and hits the documented retry/double-execute hazard); transport failures mid-poll (reconnects) are mapped to 'not yet' so polling survives reconnects; overall timeout default raised to 300s to tolerate the user walking to the car; deadline with neither path confirming raises a typed BluetoothTimeout. This is pair()'s new default behavior, not a new opt-in knob (poll_interval is just a defaulted param). Sequential fast-path-then-poll avoids the VCSEC session-lock contention that true concurrency would hit. Added tests/test_ble_pair.py (5 mocked-transport tests: reply success, reply fault raises, reply-lost->poll detects success, reconnect-mid-wait->poll continues, deadline->BluetoothTimeout), an AGENTS.md gotcha entry with the CoreBluetooth observation, and a docs note. Warrants a 1.6.2 note; do not cut a release.
What Changed
VehicleBluetooth.pair()to write the whitelist operation once, then confirm pairing through either the normal whitelist reply or polling a VCSEC handshake until the key is accepted.poll_interval, and kept reconnect/transport failures during the probe path as “not yet paired” until the overall timeout.pair()tests covering reply success, reply faults, lost replies, reconnects, and timeout behavior.Risk Assessment
Testing
Inspected the pairing diff and docs, ran the focused BLE pairing tests, generated reviewer-visible JSON evidence for the end-to-end mocked pairing outcomes, and cleaned the transient
uv.lockchange caused byuv run; all exercised behavior passed.Evidence: pair_behavior_evidence
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tesla_fleet_api/tesla/vehicle/bluetooth.py:758-pair()now only waitspoll_intervalseconds for the whitelist-operation reply, then switches to handshake polling. If the vehicle returns a real whitelist failure after that first interval, such as user denial or whitelist-full, the polling path never parses that reply and will eventually raiseBluetoothTimeoutinstead of the mappedWhitelistOperationStatus, making actionable pairing failures look like lost replies. Keep observing/parsing whitelist-op replies during the poll window, or otherwise preserve late fault replies while still using the handshake probe for lost-success recovery.tesla_fleet_api/tesla/vehicle/bluetooth.py:772-poll_intervalis public but not constrained; passing0or a negative value makes the timeout path enter a tight loop that repeatedly handshakes and sleeps for0/negative time until the overall timeout. Validatepoll_interval > 0or clamp it to a sane minimum before using it for_send()timeout andasyncio.sleep().🔧 Fix: Handle late pairing replies
1 warning still open:
tesla_fleet_api/tesla/vehicle/bluetooth.py:769- The poll loop checks for late whitelist replies only before sleeping, then exits as soon astime.monotonic() >= deadline. A success or actionable fault that lands in the VCSEC queue during the final sleep interval, but before the advertised timeout expires, is never drained andpair()raisesBluetoothTimeout. Restructure the loop to perform one final drain/probe after waking at the deadline, or check the queue again before raising.🔧 Fix: Handle final pairing reply
1 warning still open:
tesla_fleet_api/tesla/vehicle/bluetooth.py:772- Late whitelist replies are only drained before_pair_probe(), but the probe itself uses_send(..., requires="session_info"), which clears the VCSEC queue before sending and then consumes/ignores any non-session_infomessages while it waits. A denial/whitelist-full reply that arrives during that probe window can therefore be swallowed and later reported asBluetoothTimeoutinstead of the mappedWhitelistOperationStatus. Preserve and parse whitelist-operation replies seen during the probe path, or use a pair-specific probe receive loop that does not discard them.✅ **Test** - passed
✅ No issues found.
git diff --stat 02e763ed6b6079e0e55061f21b2b02a1928e5f3f..HEADrg -n "async def pair|_pair_probe|poll_interval|test_ble_pair|BluetoothTimeout|NotOnWhitelist" tesla_fleet_api tests docs AGENTS.mduv run pytest tests/test_ble_pair.py -qmkdir -p /tmp/no-mistakes-evidence/01KX5G67DABV7JTTJBBCKPC41Y && uv run python - <<'PY' ... PY(mockedVehicleBluetooth.pair()evidence run writingpair_behavior_evidence.json)git status --short✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.