Subscribe to MQTT, write to Redis. Redis will only store the latest for each MQTT username
  • Python 81%
  • Dockerfile 13.6%
  • Makefile 5.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Max Katz-Christy 3696038c6d
All checks were successful
Build / build (push) Successful in 18s
docs: link sibling repos on GitHub in README
2026-09-30 21:42:07 +02:00
.forgejo/workflows chore: sync shared repo standards 2026-09-22 13:58:46 +02:00
src/vehicle_poser feat: key positions on the vehicle, via the shared helper 2026-09-22 10:06:29 +02:00
.env.example chore: apply bare-chassis copier template 2026-04-17 00:45:22 +02:00
.gitignore chore: copier update to v0.1.0 2026-04-17 21:56:40 +02:00
.pre-commit-config.yaml build: default pre-commit 2026-03-10 15:02:55 +01:00
CHANGELOG.md bump: version 0.1.0 → 0.1.1 2026-03-10 16:55:21 +01:00
CLAUDE.md chore: sync shared repo standards 2026-09-22 13:58:46 +02:00
Dockerfile fix: install git in builder so uv can fetch railroad-club 2026-07-25 01:20:36 +02:00
LICENSE.txt agpl-3 2026-03-02 16:28:54 +01:00
Makefile chore: apply bare-chassis copier template 2026-04-17 00:45:22 +02:00
pyproject.toml build(deps): install railroad-club from GitHub 2026-09-30 19:27:19 +02:00
README.md docs: link sibling repos on GitHub in README 2026-09-30 21:42:07 +02:00
uv.lock build(deps): install railroad-club from GitHub 2026-09-30 19:27:19 +02:00

vehicle-poser

Tiny async Python service that receives Traccar position forwards over HTTP and writes normalized vehicle positions to Redis.

Part of a larger stack; see deploy-gtfs-rt for the full deployment.

How it fits together

Traccar Client app (phone)
    └─> Traccar server (:5055 osmand ingest)
            └─> forward.type=json  POST /forward
                    └─> vehicle-poser (this service)
                            └─> Redis (vehicle:{tracker_id}:{deviceId} keys, one per vehicle, 60s TTL)
                                    └─> cafe-car (serves GTFS-RT feeds)

Traccar is configured with forward.type=json / forward.url=http://vehicle-poser:8080/forward. On each POST the service reads device.uniqueId (the tracker's secret device_key) and hands it to railroad-club's resolve_tracker_trip, which returns the tracker's non-secret surrogate id, its active trip_id and the service date that run started on. This is the only place the credential is spoken: every record and Redis key downstream uses the surrogate. The payload is transformed to a normalized record and written to Redis with a 60-second TTL. cafe-car labels the vehicle in the public feed by the tracker's nickname (resolved from the DB).

A device key with no matching tracker is dropped. A tracker with no active rule is not: it gets a record with a null trip_id, which is what draws it as an unassigned vehicle rather than making it disappear.


Payload transformation

Traccar json forward sends {"position": Position, "device": Device}. Fields are mapped as follows:

Traccar field Redis record field Notes
device.uniqueId tracker_id translated: the posted value is the secret device_key, the record carries the tracker's surrogate id
- trip_id resolved server-side via resolve_tracker_trip(device_key) (schedule-based), or null
- start_date the resolved run's service date, YYYYMMDD, or null alongside a null trip_id
position.latitude, position.longitude lat, lon passed through
position.course bearing degrees
position.speed speed converted knots → m/s (×0.514444), 4 decimal places
position.fixTime timestamp ISO-8601 parsed to epoch seconds

Redis key: {VEHICLE_KEY_PREFIX}:{tracker_id}:{position.deviceId or "traccar"} (default prefix vehicle), keyed by the surrogate and overwritten on each update.


Environment variables

Variable Example Description
REDIS_URL redis://redis:6379/1 Redis connection URL including DB number
DATABASE_URL postgresql+psycopg2://.../postgres Postgres URL for tracker-rule trip resolution
HTTP_PORT 8080 Port the HTTP server listens on (default 8080)
VEHICLE_KEY_PREFIX vehicle Redis key namespace for written positions (default vehicle). Set to a shadow prefix (e.g. shadow:vehicle) for dual-run comparison so the Traccar pipeline doesn't clobber the live feed.

REDIS_URL and DATABASE_URL are required. The service exits with KeyError if either is missing.


Running

# Install dependencies (Python 3.13, uv)
uv sync

# Install git hooks (required once per clone)
uv run pre-commit install

# Run locally (requires Redis and Postgres)
REDIS_URL=redis://localhost:6379/1 \
DATABASE_URL=postgresql+psycopg2://postgres:mysecretpassword@localhost:5432/postgres \
  uv run python -m vehicle_poser.main

# Build and push are CI's job: pushing to main publishes :latest and :<short-sha>.
# `make cp` copies that short sha for the deploy-gtfs-rt manifest bump.
make cp

Testing

# Simulate a Traccar json forward (speed in knots, course in degrees)
curl -X POST http://localhost:8080/forward \
  -H 'Content-Type: application/json' \
  -d '{"device":{"uniqueId":"alice"},
       "position":{"latitude":51.5,"longitude":-0.1,"course":90,"speed":10,
                   "fixTime":"2026-07-23T12:00:00Z","deviceId":7}}'

# Verify the Redis key (use the DB set in REDIS_URL); the middle segment is
# the tracker surrogate, so find it with a scan rather than guessing
redis-cli -n 1 --scan --pattern "vehicle:*:7"

Expected Redis value (trip_id and start_date are null unless the tracker whose device_key is alice has an active rule; tracker_id is that tracker's surrogate id, not alice):

{"tracker_id": "0f0c1d...", "vehicle_id": "7", "trip_id": null, "start_date": null, "lat": 51.5, "lon": -0.1, "bearing": 90, "speed": 5.1444, "timestamp": 1784808000}