Skip to content

fix(vehicle): make BLE pairing survive lost whitelist replies#62

Merged
Bre77 merged 4 commits into
mainfrom
fm/tfa-pair-verify-poll-m3
Jul 10, 2026
Merged

fix(vehicle): make BLE pairing survive lost whitelist replies#62
Bre77 merged 4 commits into
mainfrom
fm/tfa-pair-verify-poll-m3

Conversation

@Bre77

@Bre77 Bre77 commented Jul 10, 2026

Copy link
Copy Markdown
Member

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

  • Updated 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.
  • Preserved actionable whitelist replies while polling, validated poll_interval, and kept reconnect/transport failures during the probe path as “not yet paired” until the overall timeout.
  • Added BLE pairing docs/agent notes plus mocked pair() tests covering reply success, reply faults, lost replies, reconnects, and timeout behavior.

Risk Assessment

⚠️ Medium: The change is well-scoped and improves the lost-success path, but there remains a timing window that can still mask actionable pairing failures as timeouts.

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.lock change caused by uv run; all exercised behavior passed.

Evidence: pair_behavior_evidence
{
  "intent": "VehicleBluetooth.pair confirms pairing by fast reply or by polling key effectiveness after a lost BLE reply, without re-sending the whitelist operation.",
  "results": [
    {
      "scenario": "one-shot whitelist reply survives",
      "outcome": "paired",
      "exception": null,
      "whitelist_op_writes": 1,
      "pair_probe_attempts": 0
    },
    {
      "scenario": "one-shot reply lost; key later becomes whitelisted via probe",
      "outcome": "paired",
      "exception": null,
      "whitelist_op_writes": 1,
      "pair_probe_attempts": 2
    },
    {
      "scenario": "BLE reconnect-like transport failure during polling is tolerated",
      "outcome": "paired",
      "exception": null,
      "whitelist_op_writes": 1,
      "pair_probe_attempts": 3
    },
    {
      "scenario": "no reply and no successful probe before deadline",
      "outcome": "raised",
      "exception": "BluetoothTimeout",
      "whitelist_op_writes": 1,
      "pair_probe_attempts": 4
    }
  ]
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 warning
  • ⚠️ tesla_fleet_api/tesla/vehicle/bluetooth.py:758 - pair() now only waits poll_interval seconds 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 raise BluetoothTimeout instead of the mapped WhitelistOperationStatus, 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_interval is public but not constrained; passing 0 or a negative value makes the timeout path enter a tight loop that repeatedly handshakes and sleeps for 0/negative time until the overall timeout. Validate poll_interval > 0 or clamp it to a sane minimum before using it for _send() timeout and asyncio.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 as time.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 and pair() raises BluetoothTimeout. 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_info messages while it waits. A denial/whitelist-full reply that arrives during that probe window can therefore be swallowed and later reported as BluetoothTimeout instead of the mapped WhitelistOperationStatus. 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..HEAD
  • rg -n "async def pair|_pair_probe|poll_interval|test_ble_pair|BluetoothTimeout|NotOnWhitelist" tesla_fleet_api tests docs AGENTS.md
  • uv run pytest tests/test_ble_pair.py -q
  • mkdir -p /tmp/no-mistakes-evidence/01KX5G67DABV7JTTJBBCKPC41Y && uv run python - <<'PY' ... PY (mocked VehicleBluetooth.pair() evidence run writing pair_behavior_evidence.json)
  • git status --short
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate crewmate 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.
@Bre77 Bre77 merged commit 3732a09 into main Jul 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant