feat(vehicle): add BLE broadcast listeners#72
Merged
Conversation
added 3 commits
July 12, 2026 18:37
VCSEC status broadcasts were only consumed by the one-shot per-command confirmation ladder (PR 68). Add a persistent listener API so callers can receive vehicle state passively instead of polling: typed listen_<field> methods for every VehicleStatus leaf field (lock state, sleep status, presence, all 8 closures, tonneau percent), plus an untyped listen_broadcast catch-all for payloads VehicleStatus decoding doesn't cover. Listeners fan out from the existing _on_message notification path - no second subscription - and coexist with the confirmation ladder's watcher.
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
Add a general BLE broadcast listener API to VehicleBluetooth so unsolicited vehicle-state broadcasts become passive state updates, not just command confirmations (captain-directed follow-up, not part of the current HA release - HA already gets state via Teslemetry streaming). Modeled on python-teslemetry-stream's listen_ pattern for typed per-field listeners. Assessed which VCSEC VehicleStatus fields are enumerable/typeable from the PR-68 broadcast decoding (decode_vcsec_status) and the vcsec proto: all 12 leaf fields turned out to be well-defined protobuf enums/ints (vehicleLockState, vehicleSleepStatus, userPresence, 8 closureStatuses sub-fields, detailedClosureStatus.tonneauPercentOpen), so all 12 got typed listen methods; everything NOT decoded into VehicleStatus (other VCSEC broadcast payloads like CommandStatus/whitelist/faults, and any future infotainment-domain broadcast, none observed to exist today) falls back to one untyped listen_broadcast(domain, callback) catch-all - this typed/untyped split is reported in the PR body per the task's requirement. New tesla_fleet_api/tesla/vehicle/broadcast.py mixin (BroadcastListeners) is mixed into VehicleBluetooth and dispatches from the EXISTING single _on_message notification-routing path used by the PR-68 one-shot confirmation-ladder watcher (_broadcast_watchers) - deliberately not a second BLE subscription; both mechanisms coexist and are tested together. Listener registries (_status_listeners, _domain_listeners) are created once in init and are unaffected by connect/disconnect, matching the existing queues lifecycle - no reconnect-cleanup hook was needed. Closure/tonneau-percent listeners gate on HasField since those are submessages with real proto3 presence tracking; the three scalar enum fields have no presence tracking so they fire on every status broadcast. Each listen* returns an unsubscribe() closure. Added 18 new tests (tests/test_ble_broadcast_listeners.py) using synthetic frames injected via vehicle._on_message, following the existing test_ble_broadcast_confirmation.py idiom (real _send machinery, not the fully-mocked ble_mocked_transport harness) since dispatch-on-broadcast needed the real _on_message routing path exercised - no live car access. Extended docs/bluetooth_vehicles.md with a new 'Broadcast Listeners' section documenting the listener surface as current behavior. Added one AGENTS.md entry documenting the split and the coexistence-with-confirmation-ladder design. After the initial commit, main advanced with PR #70 (BLE write-delivery-certainty classification: splits BluetoothTransportError vs BluetoothTimeout at the GATT write based on whether the write provably reached backend I/O, plus a 0600 file-mode fix for get_private_key) and PR #71 (version bump to 1.7.1) - rebased this branch onto origin/main, resolving a content conflict in AGENTS.md by keeping both PR #70's updated BluetoothUnconfirmedCommand/BluetoothCommandFailed entry plus its new write-delivery-certainty entry, and my new persistent-broadcast-listeners entry (dropping my now-superseded duplicate of the older BluetoothUnconfirmedCommand entry). docs/bluetooth_vehicles.md and tesla_fleet_api/tesla/vehicle/bluetooth.py auto-merged cleanly since PR #70's _send rewrite and my _on_message dispatch addition touch different regions of the file. Re-ran pyright (tesla_fleet_api strict, 0 errors), ruff check/format (clean on all touched files), and the full pytest suite (312 passed) after the rebase.
What Changed
BroadcastListenersmixin forVehicleBluetoothwith persistent typedlisten_<field>vehicle-status listeners and an untypedlisten_broadcast(domain, callback)fallback for raw unsolicited broadcasts._on_messagepath so passive listeners coexist with command-confirmation watchers, with unsubscribe closures and isolated callback failures.Risk Assessment
Testing
Exercised the new broadcast listener API through focused unit tests, adjacent BLE routing/write-timeout regression suites, the full repository test suite, and a consumer-style synthetic broadcast run that produced JSON evidence; no UI screenshot was captured because this change exposes a Python library API rather than a rendered surface.
Evidence: Consumer-style BLE broadcast listener evidence
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tesla_fleet_api/tesla/vehicle/broadcast.py:62- The listener dispatch runs user-supplied callbacks synchronously inside_on_message, which is itself called from the BLE notification reassembly loop. If any registered callback raises, the exception propagates out of_dispatch_domain_listeners/_dispatch_status_listeners, aborting the current notification’s message loop and preventing later callbacks for the same broadcast from running; with multiple frames in one BLE notification, later addressed replies can be left buffered until another notification arrives. Wrap each callback invocation intry/except Exceptionand log/drop the callback failure so listener code cannot break BLE routing.🔧 Fix: Isolate BLE broadcast listener failures
1 warning still open:
tesla_fleet_api/tesla/vehicle/broadcast.py:100- The scalar typed listeners forward proto3 default enum values unconditionally. AVehicleStatusbroadcast that only carriesclosureStatusesordetailedClosureStatuswill still calllisten_vehicle_lock_statewithVEHICLELOCKSTATE_UNLOCKED(0), and similarly default sleep/user-presence values, even though those fields were not actually present on the wire. That can turn an unrelated closure/tonneau broadcast into a false passive state update; confirm this is intended or change the scalar listener contract before exposing it as reliable state.✅ **Test** - passed
✅ No issues found.
uv run pytest tests/test_ble_broadcast_listeners.pyuv run pytest tests/test_ble_broadcast_confirmation.py tests/test_ble_write_timeout_router.py tests/test_ble_send_transport.pyuv run pytest testsAttempted evidence generation withpython - <<'PY' ...; setup issue found becausepythonis not on PATH.uv run python - <<'PY' ...generated/tmp/no-mistakes-evidence/01KXAQMYDPMD72X387MYY7AADF/ble_broadcast_listener_api_evidence.json✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.