Skip to content

feat(vehicle): add BLE broadcast listeners#72

Merged
Bre77 merged 3 commits into
mainfrom
fm/tfa-ble-broadcast-listeners
Jul 12, 2026
Merged

feat(vehicle): add BLE broadcast listeners#72
Bre77 merged 3 commits into
mainfrom
fm/tfa-ble-broadcast-listeners

Conversation

@Bre77

@Bre77 Bre77 commented Jul 12, 2026

Copy link
Copy Markdown
Member

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

  • Added a BroadcastListeners mixin for VehicleBluetooth with persistent typed listen_<field> vehicle-status listeners and an untyped listen_broadcast(domain, callback) fallback for raw unsolicited broadcasts.
  • Routed decoded BLE broadcasts through the existing _on_message path so passive listeners coexist with command-confirmation watchers, with unsubscribe closures and isolated callback failures.
  • Documented the BLE broadcast listener API in the README and Bluetooth vehicle docs, and added focused tests for typed listener dispatch, raw broadcast delivery, unsubscribe behavior, reconnect persistence, and callback failure isolation.

Risk Assessment

⚠️ Medium: The change is well scoped to the new BLE broadcast listener surface, but the scalar listener semantics can emit misleading state updates unless the team explicitly accepts that proto3 default-value ambiguity.

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
{
  "scenario": "Consumer registers persistent BLE broadcast listeners and receives passive updates from unsolicited VCSEC broadcasts.",
  "api_methods_exercised": [
    "listen_vehicle_lock_state",
    "listen_vehicle_sleep_status",
    "listen_user_presence",
    "listen_front_driver_door",
    "listen_broadcast",
    "unsubscribe callback returned by listen_vehicle_lock_state"
  ],
  "typed_listener_events": [
    {
      "listener": "listen_vehicle_lock_state",
      "value": "VEHICLELOCKSTATE_LOCKED"
    },
    {
      "listener": "listen_vehicle_sleep_status",
      "value": "VEHICLE_SLEEP_STATUS_AWAKE"
    },
    {
      "listener": "listen_user_presence",
      "value": "VEHICLE_USER_PRESENCE_PRESENT"
    },
    {
      "listener": "listen_front_driver_door",
      "value": "CLOSURESTATE_OPEN"
    },
    {
      "listener": "listen_vehicle_sleep_status",
      "value": "VEHICLE_SLEEP_STATUS_ASLEEP"
    },
    {
      "listener": "listen_user_presence",
      "value": "VEHICLE_USER_PRESENCE_UNKNOWN"
    },
    {
      "listener": "listen_vehicle_sleep_status",
      "value": "VEHICLE_SLEEP_STATUS_UNKNOWN"
    },
    {
      "listener": "listen_user_presence",
      "value": "VEHICLE_USER_PRESENCE_NOT_PRESENT"
    }
  ],
  "generic_broadcast_events": [
    {
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "has_vehicle_status": true,
      "bytes": 12
    },
    {
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "has_vehicle_status": true,
      "bytes": 4
    },
    {
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "has_vehicle_status": false,
      "bytes": 2
    },
    {
      "domain": "DOMAIN_VEHICLE_SECURITY",
      "has_vehicle_status": true,
      "bytes": 4
    }
  ],
  "checks_demonstrated": {
    "typed_vehicle_status_fields_delivered": true,
    "generic_listener_receives_vehicle_status_and_non_status_payloads": [
      true,
      true,
      false,
      true
    ],
    "unsubscribe_stopped_second_lock_update": [
      {
        "listener": "listen_vehicle_lock_state",
        "value": "VEHICLELOCKSTATE_LOCKED"
      }
    ],
    "listeners_survived_disconnect": true
  }
}

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/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 in try/except Exception and 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. A VehicleStatus broadcast that only carries closureStatuses or detailedClosureStatus will still call listen_vehicle_lock_state with VEHICLELOCKSTATE_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.py
  • uv run pytest tests/test_ble_broadcast_confirmation.py tests/test_ble_write_timeout_router.py tests/test_ble_send_transport.py
  • uv run pytest tests
  • Attempted evidence generation with python - &lt;&lt;&#39;PY&#39; ...; setup issue found because python is not on PATH.
  • uv run python - &lt;&lt;&#39;PY&#39; ... 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.

firstmate crewmate 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.
@Bre77 Bre77 merged commit 0ad4fb8 into main Jul 12, 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