Skip to content

feat(ble): add BleBroadcastStreamGlue for teslemetry-stream ingest() - #139

Merged
Bre77 merged 3 commits into
mainfrom
fm/tfa-ble-broadcast-stream-glue
Aug 28, 2026
Merged

Bre77 merged 3 commits into
mainfrom
fm/tfa-ble-broadcast-stream-glue

Conversation

@Bre77

@Bre77 Bre77 commented Aug 28, 2026

Copy link
Copy Markdown
Member

Intent

Build BleBroadcastStreamGlue in python-tesla-fleet-api, per the approved design report (captain approved 2026-08-28). Takes BLE broadcast observations from the existing broadcast listener (tesla_fleet_api/tesla/vehicle/broadcast.py) and publishes them into teslemetry-stream's already-shipped ingest() surface, so BLE observations reach the same stream listeners a native SSE event does, with no translation and no second subscription.

Hard design constraints from the approved report:

  • Duck-typed, NO import of teslemetry_stream. The glue calls .ingest(data, metadata) on a sink it is handed. Defines a local structural Protocol (StreamSink: has ingest(data, metadata=None)), matching the aiopowerwall router shim precedent (router/energysite.py composes a duck-typed PowerwallEnergySite with no dependency added). Net new dependency must be zero - pyproject.toml and uv.lock are verified unchanged.
  • Output IS the stream shape, BLE translated INTO it; source identity rides a metadata dict ({"source": "bluetooth", "raw": }); there is deliberately NO source ranking/precedence, matching the captain's standing rule for the funnel/router design elsewhere in this codebase.
  • teslemetry-stream needed zero changes (its ingest() already exists from its own PR fix(vehicle): standardize auto seat climate on 1-indexed AutoSeat (fixes #11) #41) - that repo was not touched.
  • Colocated in tesla_fleet_api/tesla/vehicle/stream_glue.py next to the broadcast listeners it wires up, matching the library's existing style (BleBroadcastPublisher in funnel.py, BroadcastListeners in broadcast.py).

What was built:

  • BleBroadcastStreamGlue(vehicle, sink): wires listen_vehicle_lock_state/listen_charge_port/listen_front_trunk to sink.ingest() calls, reusing funnel.py's existing LOCK_STATES/CLOSURE_STATES decode maps (promoted from module-private to module-level, since they needed to be imported cross-module - this was the one change to funnel.py, purely a visibility rename with no behavior change).
  • stop() lifecycle method (idempotent) so a consumer (e.g. a Home Assistant integration) can register it via entry.async_on_unload(glue.stop).
  • Tests in tests/test_ble_stream_glue.py: broadcast-to-ingest() translation and shape correctness for all three fields, ambiguous/absent states producing no call, the no-import/duck-typed contract (AST-checked, plus a plain-object sink with no shared base class), zero-net-new-dependency assertion against pyproject.toml, and stop()/idempotency lifecycle coverage.
  • Documented in docs/bluetooth_vehicles.md (new "Feeding broadcasts into a Teslemetry stream" section) and AGENTS.md.

Explicitly out of scope (a separate parked task): the BLE-POLLED "Merge" class that merges polled BLE data with cloud vehicle_data - this PR is broadcasts-only. The HA integration migration that consumes this glue is a separate follow-on task.

Full test suite (749 tests), ruff check, ruff format, and pyright strict all pass locally.

What Changed

  • Add BleBroadcastStreamGlue (tesla_fleet_api/tesla/vehicle/stream_glue.py) which wires the existing listen_vehicle_lock_state/listen_charge_port/listen_front_trunk broadcast listeners to a duck-typed StreamSink (ingest(data, metadata=None)), so BLE broadcasts push into a teslemetry-stream sink in the same shape a native SSE event would, tagged with metadata={"source": "bluetooth", "raw": <proto enum name>}; includes an idempotent stop() lifecycle method and no import of teslemetry_stream.
  • Promote LOCK_STATES/CLOSURE_STATES in tesla_fleet_api/funnel.py from module-private to module-level so stream_glue.py can reuse the same decode maps (visibility rename only, no behavior change); export BleBroadcastStreamGlue from tesla_fleet_api/__init__.py.
  • Add tests/test_ble_stream_glue.py covering broadcast-to-ingest() translation for all three fields, ambiguous/absent states producing no call, the no-import/duck-typed contract, and stop()/idempotency; document the new glue in docs/bluetooth_vehicles.md and AGENTS.md.

Risk Assessment

✅ Low: The change matches the approved design exactly (duck-typed StreamSink Protocol, no teslemetry_stream import, zero new dependency, correct translation logic reusing funnel.py's decode maps), both prior test-quality findings were properly fixed with genuine behavioral assertions (sys.modules check, tomllib-based semantic parse), and no other functional or structural issues were found in the implementation, exports, or docs.

Testing

Ran the focused test_ble_stream_glue.py suite plus the funnel/broadcast-listener tests it depends on — all 67 tests pass, and manual diff inspection confirms both flagged AST/substring test-quality issues were replaced with genuine behavioral assertions (sys.modules check after exercising the glue; tomllib-parsed dependency list check) as decided in review round 1.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ tests/test_ble_stream_glue.py:238 - test_module_source_never_references_teslemetry_stream proves nothing about runtime behavior: it calls inspect.getsource on stream_glue and walks the parsed AST for Import/ImportFrom nodes naming teslemetry_stream. This is a source-content-only assertion (an AST-shape check) — a behavior-preserving refactor (e.g. a differently-shaped conditional import, or an import hidden inside a string built dynamically) could dodge the exact AST pattern checked while a legitimate rewrite that changes unrelated code structure could break it for no behavioral reason. A behavioral replacement is available and cheap: assert &#39;teslemetry_stream&#39; not in sys.modules after importing and exercising stream_glue, which proves the module was never actually imported rather than that its source text happens not to contain an import statement matching a string.
  • ⚠️ tests/test_ble_stream_glue.py:270 - test_zero_net_new_dependency reads pyproject.toml as raw text and does assertNotIn(&#34;teslemetry-stream&#34;, text) / assertNotIn(&#34;teslemetry_stream&#34;, text). This is exactly the anti-pattern the test-quality rule calls out for machine-consumed declarative config: a raw substring search over the file rather than parsing it into a semantic model. It would false-negative on a match hiding in an unrelated comment/URL, and false-positive-pass if the string appeared only in, say, a # see teslemetry_stream comment while a real dependency was actually added under a different alias. Parse with tomllib and assert the string is absent from the actual project.dependencies (and [tool.uv.sources]/optional-dependencies as applicable) list instead.

🔧 Fix: Replace AST/substring test-quality checks with behavioral tomllib assertions
1 info still open:

  • ℹ️ tests/test_ble_stream_glue.py:273 - _names()'s dependency-spec parser only strips &gt;= and == operators before comparing names. A dependency spec using another PEP 508 operator (~=, &lt;=, &lt;, &gt;, !=) or an extras marker (pkg[extra]) would leave the operator/version attached to the parsed name, so assertNotIn(&#34;teslemetry-stream&#34;, dependency_names) would still pass even if such a spec were added, since the set element would be the unparsed string rather than the bare name. Minor gap in an otherwise fixed semantic-parse test; not a blocker.
✅ **Test** - passed

✅ No issues found.

  • uv run pytest tests/test_ble_stream_glue.py -v (12 passed)
  • uv run pytest tests/test_funnel.py tests/test_ble_broadcast_listeners.py -q (55 passed) — sanity check on funnel.py's LOCK_STATES/CLOSURE_STATES visibility rename that stream_glue.py depends on
  • manual diff review of git show HEAD -- tests/test_ble_stream_glue.py confirming the AST/inspect-based check was replaced with a behavioral assertNotIn(&#34;teslemetry_stream&#34;, sys.modules) around real BleBroadcastStreamGlue usage, and the raw-text pyproject.toml substring check was replaced with a tomllib.load parse checked against actual dependency/source name sets
  • confirmed the now-unused from tesla_fleet_api.tesla.vehicle import stream_glue module import was removed alongside the AST check that used it
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Bre77 added 2 commits August 28, 2026 09:55
Feeds VCSEC broadcast listeners (lock state, charge port, front trunk)
into any duck-typed sink shaped like python-teslemetry-stream's
ingest(data, metadata) - no import of teslemetry_stream, same
structural-Protocol pattern EnergySiteRouter uses for aiopowerwall.

Promotes funnel.py's LOCK_STATES/CLOSURE_STATES decode maps to
module-level so the glue can reuse them without a pyright
reportPrivateUsage violation.
@Bre77 Bre77 added the fm Opened by a Firstmate crewmate label Aug 28, 2026
Comment thread tesla_fleet_api/tesla/vehicle/stream_glue.py Outdated
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
@Bre77
Bre77 merged commit fc7ecbb into main Aug 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fm Opened by a Firstmate crewmate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant