fix(ble): wrap proxy transport timeouts#57
Merged
Conversation
added 3 commits
July 10, 2026 09:43
VehicleBluetooth._send and connect() wrapped only bleak.exc.BleakError from their bleak-esphome client calls in BluetoothTransportError. But bleak-esphome's @api_error_as_bleak_error decorator converts an aioesphomeapi TimeoutAPIError into a builtin TimeoutError (not a BleakError), so a GATT-write, connect, or start_notify timeout escaped the wrap as a bare TimeoutError - not a TeslaFleetError, defeating the documented `except TeslaFleetError` contract. Reproduced live over an ESPHome proxy (adjust_volume and wake paths both raised `TimeoutError: Timeout waiting for BluetoothGATTWriteResponse ... after 30.0s`). Broaden both catch sites to (BleakError, TimeoutError) so every transport-layer timeout surfaces as BluetoothTransportError with the original exception chained. Add regression tests covering the write, establish_connection, and start_notify timeout paths (each fails before this change, leaking a bare TimeoutError).
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
Fix a confirmed BLE exception-typing bug found live during a captain-present BLE verification session. VehicleBluetooth._send (GATT write) and connect() (establish_connection + start_notify) wrapped only bleak.exc.BleakError in BluetoothTransportError, but bleak-esphome's @api_error_as_bleak_error decorator converts an aioesphomeapi TimeoutAPIError into a builtin TimeoutError (not a BleakError). So a GATT-write, connect, or start_notify timeout over an ESPHome proxy escaped the wrap as a bare TimeoutError - which is NOT a TeslaFleetError, defeating the library's documented 'catch TeslaFleetError and you're covered' contract (the same contract PR #55 established). Reproduced live twice over an ESPHome proxy (adjust_volume and the wake path both raised 'TimeoutError: Timeout waiting for BluetoothGATTWriteResponse ... after 30.0s'). Deliberate decision: broaden BOTH catch sites from 'except BleakError' to 'except (BleakError, TimeoutError)' so every transport-layer timeout surfaces as BluetoothTransportError with the original exception chained as cause; BluetoothTransportError (not BluetoothTimeout) is the right target because it is what the same call sites already map BleakError to, and a write/connect timeout is a transport failure distinct from the response-wait BluetoothTimeout. Added 3 regression tests in tests/test_ble_send_transport.py (write, establish_connection, start_notify timeout paths) - each fails before the fix, leaking a bare TimeoutError, and passes after. Updated the AGENTS.md Error Handling note to state both exception types are caught and why. Scope deliberately minimal: only the two catch sites plus tests and the doc note; no behavior change beyond exception typing. A BLE test harness under scripts/ble-harness/ is intentionally git-excluded and not part of this change.
What Changed
TimeoutErrorfailures from GATT writes, connection setup, and notification setup asBluetoothTransportError, preserving the original exception as the cause so callers can continue catchingTeslaFleetError.start_notifytimeout paths, and update BLE transport docs to describe the widened transport error handling.Risk Assessment
✅ Low: The changed code is narrowly scoped to wrapping BLE transport TimeoutError paths and cleaning up partial connect state; I do not see a remaining merge-blocking or follow-up-worthy defect in the diff.
Testing
Inspected the BLE transport diff, ran the focused BLE transport regression file and the full test suite, then produced caller-level evidence showing write/connect/start-notify builtin
TimeoutErrors are caught asTeslaFleetErrorviaBluetoothTransportErrorwith the original cause preserved; all checks passed and transient pytest caches were removed.Evidence: BLE transport timeout caller-contract transcript
BLE transport timeout caller contract BluetoothTransportError is TeslaFleetError: True GATT write TimeoutError: caught=BluetoothTransportError; cause=TimeoutError; preserved=True establish_connection TimeoutError: caught=BluetoothTransportError; cause=TimeoutError; preserved=True start_notify TimeoutError: caught=BluetoothTransportError; cause=TimeoutError; preserved=TruePipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
tesla_fleet_api/tesla/vehicle/bluetooth.py:272- Whenstart_notify()raises a newly wrappedTimeoutError,self.clienthas already been assigned to the connected client. A caller that catchesBluetoothTransportErrorand retries viaconnect_if_needed()can then skipconnect()becauseself.client.is_connectedis still true, leaving notifications unregistered; subsequent_send()calls can write successfully but never receive queued replies and degrade intoBluetoothTimeout. Clear or disconnectself.clienton anyconnect()setup failure before raisingBluetoothTransportError, and cover thestart_notifyfailure case in the regression test.🔧 Fix: Reset BLE client after setup failure
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Inspected changed files withgit diff --stat 2300b74fbd1668496ae5653cdf18865b73fd37e8..b10e7ad677bd85226105a708815517247240f4d7,git diff --name-only 2300b74fbd1668496ae5653cdf18865b73fd37e8..b10e7ad677bd85226105a708815517247240f4d7, and targeted diffs forbluetooth.py/test_ble_send_transport.py.uv run pytest tests/test_ble_send_transport.pyuv run python - <<'PY' > /tmp/no-mistakes-evidence/01KX4MB0MKKVHZ872480NS3HPH/ble_transport_timeout_contract.txt ... PYuv run pytest testsCleaned pytest transient artifacts withfind . -type d \( -name '__pycache__' -o -name '.pytest_cache' \) -prune -exec rm -rf {} +and confirmedgit status --shortwas clean.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.