Skip to content

fix(ble): wrap transport failures in library errors#55

Merged
Bre77 merged 2 commits into
mainfrom
fm/tfa-ble-transport-wrap-d4
Jul 9, 2026
Merged

fix(ble): wrap transport failures in library errors#55
Bre77 merged 2 commits into
mainfrom
fm/tfa-ble-transport-wrap-d4

Conversation

@Bre77

@Bre77 Bre77 commented Jul 9, 2026

Copy link
Copy Markdown
Member

Intent

  • Wrap BLE transport-layer failures in a TeslaFleetError subclass so callers get one catchable hierarchy
    • Evidence: scout report tfa-ble-edgecase-rel-n4 §2a-2 (deterministic, real _send) — a mid-write GATT failure (write_gatt_char, before the asyncio.timeout guard) and a connect/connect_if_needed failure both propagated as raw bleak.exc.BleakError, uncaught by except TeslaFleetError. Only the response-wait timeout was wrapped (BluetoothTimeout).
  • Fix approach: exception wrapping only — no retry changes, no timeout changes, no other behavior changes
    • Added BluetoothTransportError(TeslaFleetError) in exceptions.py
    • Wrapped the connect() BleakError path (which connect_if_needed() also goes through) and the mid-command write_gatt_char BleakError path in _send(), both re-raising from e to preserve the chained cause
    • Updated the VehicleBluetooth class docstring and one line in AGENTS.md (the existing "catch TeslaFleetError or BaseException" guidance) to state the new catch contract
  • Implication: behavioral change to flag for reviewers
    • Callers previously catching raw BleakError directly will now see BluetoothTransportError instead, with the original BleakError chained as __cause__
  • Scope boundaries (deliberately not done)
    • aioesphomeapi errors are left unwrapped — aioesphomeapi is not a dependency of this library (only reachable transitively via an ESPHome BLE proxy), so there is no clean import path without adding a new dependency; accepted scope boundary, not an oversight
    • No retry/timeout logic changed; purely an exception-type change

What Changed

  • Added BluetoothTransportError, a TeslaFleetError subclass, and wrapped BLE connection and GATT write BleakError failures so callers can catch transport failures through the library error hierarchy while preserving the original cause.
  • Updated BLE documentation to describe the new wrapped transport-error contract for VehicleBluetooth callers.
  • Expanded BLE send transport tests to cover write failures, connect failures, and connect_if_needed() propagation.

Risk Assessment

✅ Low: The change is tightly scoped to wrapping existing BLE connect/write transport failures in the library exception hierarchy, preserves exception causes, and adds focused regression coverage without broad behavioral churn.

Testing

Exercised the targeted BLE transport tests, the full pytest suite, and a consumer-style runtime probe showing both GATT write and connect failures are caught by except TeslaFleetError as BluetoothTransportError with the original BleakError preserved as __cause__; all checks passed.

Evidence: BLE transport catch-contract transcript
write failure caught by except TeslaFleetError
  wrapped type: BluetoothTransportError
  original cause: BleakError: write failed
connect_if_needed failure caught by except TeslaFleetError
  wrapped type: BluetoothTransportError
  original cause: BleakError: connect failed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • uv run pytest tests/test_ble_send_transport.py -q
  • uv run pytest tests -q
  • uv run python - <<'PY' | tee /tmp/no-mistakes-evidence/01KX4DTQ8YYQRAEXN51K8QWVB9/ble_transport_error_contract.txt
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Full narrative / original brief

Wrap BLE transport-layer failures in a TeslaFleetError subclass so callers get one catchable hierarchy. Evidence (scout report tfa-ble-edgecase-rel-n4, section 2a-2, deterministic real _send probes): a mid-write GATT failure (write_gatt_char, bluetooth.py, before the asyncio.timeout guard) and a connect_if_needed/connect failure both propagated as raw bleak.exc.BleakError, not caught by 'except TeslaFleetError'. Only the response-wait timeout was wrapped (BluetoothTimeout). This is a small, scoped hardening PR (exception wrapping ONLY - no retry changes, no timeout changes, no other behavior changes): added BluetoothTransportError(TeslaFleetError) in exceptions.py, wrapped the connect() BleakError path (which connect_if_needed() also goes through) and the mid-command write_gatt_char BleakError path in _send(), both re-raising 'from e' to preserve the chained cause. Deliberately left aioesphomeapi errors unwrapped since aioesphomeapi is not a dependency of this library (only reachable transitively via an ESPHome BLE proxy) and there's no clean import path without adding a new dependency - this is a known, accepted scope boundary, not an oversight. Updated the VehicleBluetooth class docstring and one line in AGENTS.md (the existing 'catch TeslaFleetError or BaseException' guidance) to state the new catch contract - callers catching just TeslaFleetError now also catch BLE transport failures, not only BluetoothTimeout. This is a behavioral change worth flagging: callers who were previously catching raw BleakError directly will now see BluetoothTransportError instead (with the original BleakError chained as cause). Added 3 new tests to tests/test_ble_send_transport.py using the existing mocked-transport pattern (mocked GATT client / mocked establish_connection) covering: the mid-write GATT failure path, the connect() establish_connection failure path, and confirming connect_if_needed() propagates the same wrapped error. Ran ruff check, ruff format, pyright (strict), and the full pytest suite locally - all clean/passing (156 tests).

firstmate crewmate added 2 commits July 10, 2026 07:50
connect_if_needed()/connect() and the mid-command GATT write in _send()
previously let raw bleak.exc.BleakError escape uncaught, so a caller doing
except TeslaFleetError missed transport failures entirely (only the
response-wait timeout was wrapped, as BluetoothTimeout). Wrap both paths
in a new BluetoothTransportError(TeslaFleetError), chaining the original
BleakError as the cause, so VehicleBluetooth failures are catchable
through one hierarchy.
@Bre77 Bre77 merged commit 9ecb181 into main Jul 9, 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