Skip to content

feat(tpms): slice-1 ingest + JSON API for Mazda CX-9 - #11

Merged
tclancy merged 2 commits into
mainfrom
claude/7-tpms-ingest
Jul 13, 2026
Merged

feat(tpms): slice-1 ingest + JSON API for Mazda CX-9#11
tclancy merged 2 commits into
mainfrom
claude/7-tpms-ingest

Conversation

@metaframework-dispatch-bot

Copy link
Copy Markdown

Summary

Slice-1 of TPMS ingest for the 2013 Mazda CX-9, per Tom's scoping answers on #7:

  • Capture: rtl_433 -M utc -f 315000000 -R 156 -F json → stdout (Abarth-124Spider decoder covers Mazda VDO sensors at 315 MHz per issue Research Task: Tire Pressure Monitoring #1 research).
  • Ingest (src/tpms/ingest.py): reads that JSON stream from stdin, filters to the Abarth-124Spider decoder, normalizes pressure to kPa + temperature to °C + battery_ok, upserts into SQLite. Survives malformed lines and non-target decoder events without dropping the pipe.
  • API (src/tpms/api.py): Flask app on :8090:
    • GET /api/vehicles — vehicle set (mazda-cx9 only in slice-1).
    • GET /api/vehicles/mazda-cx9/tpms/latest — one row per known sensor, plus any_low / all_stale flags for the future dashboard.
    • GET /api/vehicles/mazda-cx9/tpms/history?since=&until=&limit= — time-window slice.
    • GET /api/healthlast_event_ts, receiver_ok, so the dashboard can distinguish sensors-quiet from receiver-broken.
  • Data model (src/tpms/db.py): vehicles + sensors + readings. Slice-1 seeds one vehicle; positions stay NULL (Tom's Q4 answer — any-tire-low is sufficient).
  • Systemd (deploy/plexpi/tpms-{capture,api}.service) — systemctl --user units targeting the pi account on plexpi. plexpi has no itguy, so deploy/plexpi/README.md has the manual install ritual (bluelinky/fuelbot pattern).

Scope explicitly deferred: sensor→corner mapping, drive-time reception, other vehicles, dashboard UI.

Design notes

  • rtl_433 must be invoked with -M utc — without it, timestamps are system-local with no tz suffix and the ingest daemon would skew every reading by the local UTC offset. Captured in both the systemd unit and the module docstring.
  • API opens plain sqlite3 connections per request after create_app() runs schema init once. Concurrent reader alongside the ingest writer — WAL is on.
  • Duplicate readings drop silently on (ts, sensor_id) PK conflict — rtl_433 sometimes re-emits an identical event.

Test plan

  • 43 unit tests pass (21 new + 22 pre-existing). DB idempotency, ingest survives malformed input, non-target vehicle filtering, PSI→kPa fallback, stale + any_low semantics, unknown-vehicle 404, history bounded by since/until, health with empty DB.
  • End-to-end smoke: piped a real JSON event into python -m src.tpms.ingest, then hit every API endpoint. See the captured session below.
Captured test run (showboat)

TPMS slice-1 test run

2026-07-13T00:23:41Z by Showboat 0.6.1

uv run pytest
warning: `VIRTUAL_ENV=/Users/tom/Documents/work/metaframework/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
============================= test session starts ==============================
platform darwin -- Python 3.13.7, pytest-9.0.2, pluggy-1.6.0
rootdir: /private/tmp/rf-wt-1783901697-50961
configfile: pyproject.toml
testpaths: tests
plugins: anyio-4.13.0
collected 43 items

tests/test_cli.py .....                                                  [ 11%]
tests/test_device.py .................                                   [ 51%]
tests/tpms/test_api.py .......                                           [ 67%]
tests/tpms/test_db.py ......                                             [ 81%]
tests/tpms/test_ingest.py ........                                       [100%]

============================== 43 passed in 0.18s ==============================

Closes #7

🤖 Generated with Claude Code

dispatch-bot[bot] and others added 2 commits July 12, 2026 20:19
rtl_433 (register #156, 315 MHz) streams JSON on stdout; a Python ingest daemon
reads its stdin, filters to the Abarth-124Spider decoder (matches Mazda VDO
sensors), normalizes to kPa + °C + battery_ok, and upserts into SQLite. A small
Flask app exposes /api/vehicles, /api/vehicles/mazda-cx9/tpms/{latest,history},
and /api/health for a future home dashboard.

Scope kept to what Tom greenlit on the issue: parked-only, Mazda-only,
any-tire-low (no sensor→corner mapping, positions stay NULL). Systemd units
for plexpi ship under deploy/plexpi/ alongside a manual install README —
plexpi has no itguy so this follows the bluelinky/fuelbot convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six findings from the review, all real:

1. rtl_433 emits system-local time by default — capture unit now passes `-M
   utc` and the docstring on `_parse_time` documents the assumption. Without
   this every stored ts would be skewed by the local UTC offset.
2. `User=`/`Group=pi` are rejected in `systemctl --user` units. Removed and
   moved data + log paths under `%h` so no sudo is needed.
3. `history_for_vehicle` joined sensors on `sensor_id` alone — TPMS IDs aren't
   globally unique; when a second vehicle is ever seeded a collision would
   cross-contaminate the results. Join is now on (vehicle_id, sensor_id).
4. Dropped the stale `Wants=tpms-ingest.socket` — copy-paste from an
   abandoned socket-activation design.
5. API `_connect` re-ran the full schema script + vehicle upsert on every
   request. Now: init once at app startup, per-request handles are plain
   sqlite3 connections.
6. `main()` in ingest had a dead-branch `return 0 if run() >= 0 else 1` —
   `run()` never returns a negative count. Simplified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@tclancy
tclancy merged commit d87a0ef into main Jul 13, 2026
1 check passed
@tclancy
tclancy deleted the claude/7-tpms-ingest branch July 13, 2026 11:13
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.

Research: Tire Pressure Monitors for 2013 Mazda CX-9

1 participant