Streams your Tesla's positions from TeslaMate into Dawarich, live, and backfills your whole driving history in one command.
A single Go binary. No queue, no broker requirement, no state beyond one small cursor file.
- How it works
- Setup — reaching Dawarich, HTTPS, API key
- Choosing what gets synced — from now on, from a date, or the whole history
- Checking that it works
- Configuration and flags
- What ends up in Dawarich
TeslaMate already records every position it gets from the car in its Postgres
positions table, with an accurate timestamp. This daemon reads new rows from
there and posts them to Dawarich's Overland batch endpoint.
Reading from Postgres instead of MQTT matters:
- Real timestamps. MQTT publishes latitude and longitude on separate topics with no timestamp, so an MQTT-only importer has to guess when a point was recorded and pair two topics by hand.
- No gaps. If the daemon, the network or Dawarich is down for an hour, the rows are still in Postgres and the next pass picks them up. Nothing is lost and no local queue is needed.
- Backfill is the same code path. Point the cursor at 2019 and the live syncer becomes a history importer.
Setting MQTT_HOST subscribes to teslamate/cars/+/latitude and uses it purely
as a nudge: a published position triggers an immediate poll instead of
waiting for the next tick. Latency drops to about a second while the data still
comes from Postgres.
The cursor is the timestamp of the newest position already sent, and every pass
re-reads a five-minute window before it. TeslaMate assigns positions.id before
the transaction commits, so a strictly increasing id cursor can silently skip a
row that becomes visible late; a time window with overlap cannot.
Re-sending costs nothing because Dawarich upserts points on
(longitude, latitude, timestamp, user). Duplicates are impossible, so retries,
overlaps and repeated backfills are all safe. The cursor advances only after
Dawarich has accepted a batch, so a failure mid-sync resumes instead of skipping.
The daemon needs two things: TeslaMate's Postgres and Dawarich's HTTP port. The
easiest place to get both is TeslaMate's own docker-compose.yml, because
adding the service there puts it on the same Compose network as database and
mosquitto — both then resolve by name, exactly as they do for TeslaMate itself.
Add this service to your TeslaMate docker-compose.yml:
services:
teslamate-dawarich:
image: lukstankovic/teslamate-dawarich:latest
restart: unless-stopped
depends_on:
- database
environment:
DAWARICH_URL: http://dawarich_app:3000
DAWARICH_API_KEY: your_dawarich_api_key
# Same credentials TeslaMate uses; a read-only role works too.
TESLAMATE_DB_URL: postgres://teslamate:${DATABASE_PASS}@database:5432/teslamate
# Optional: sync the moment a position is published instead of polling.
MQTT_HOST: mosquitto
LOG_LEVEL: info
volumes:
# Holds the sync cursor. Without it, a restart re-syncs INITIAL_LOOKBACK.
- teslamate-dawarich-data:/data
volumes:
teslamate-dawarich-data:Keep it a named volume. The container runs as a non-root user, and a named
volume inherits /data's ownership from the image; a bind mount to a host
directory does not, so the cursor could not be written.
Wire it to Dawarich as described below, then start it:
docker compose up -d teslamate-dawarichTeslaMate and Dawarich run in separate Compose projects, so they are on separate networks. Pick one of the two options below.
Dawarich's compose file declares a network called dawarich, which Compose
prefixes with its project name. Find the real name:
docker network ls | grep dawarichThen add it to the service and declare it as external, keeping
DAWARICH_URL: http://dawarich_app:3000 from above:
services:
teslamate-dawarich:
# ... the service from above, plus:
networks:
- default # keeps database and mosquitto reachable
- dawarich
networks:
dawarich:
external: true
name: dawarich_dawarich # whatever `docker network ls` printedSkip the network plumbing and point at the host, using its LAN address rather
than localhost — inside a container that means the container itself:
services:
teslamate-dawarich:
environment:
DAWARICH_URL: http://192.168.1.10:3000Dawarich and TeslaMate's Grafana both default to host port 3000, so on a shared host one of them is published elsewhere. Use the port Dawarich actually listens on.
Setting APPLICATION_PROTOCOL=https turns on Rails' force_ssl, so Dawarich
answers plain HTTP with a redirect. The daemon refuses to follow redirects — a
redirected POST would silently become a GET — and tells you what to do instead:
- point
DAWARICH_URLat the HTTPS address, or - keep the internal HTTP address and set
DAWARICH_FORWARDED_PROTO=https, the same trick Dawarich's own health check uses.
Dawarich also authorises the Host header against APPLICATION_HOSTS. Whatever
name you put in DAWARICH_URL must be listed there, so reaching a container by
its service name means adding dawarich_app to that list. An address that is
already allowed — the LAN IP or the public domain — needs no change.
A public URL works too, but an internal address avoids two traps: routers that cannot reach your own public IP from inside the LAN, and the proxy or CDN in front of a backfill's hundreds of requests. To keep one URL where hairpinning fails, resolve the domain to the internal address for this container only. TLS still validates, because the hostname is unchanged:
services:
teslamate-dawarich:
environment:
DAWARICH_URL: https://dawarich.example.com
extra_hosts:
- "dawarich.example.com:192.168.1.10"In Dawarich: user menu → Settings → API key (the same key /api-docs
refers to). Generating a new one there invalidates the old one, so update the
environment if you regenerate it.
Two settings decide it, and they are independent:
| controls | where | |
|---|---|---|
| time range | how far back to start | INITIAL_LOOKBACK, or the -from / -full flags |
| position kind | drives only, or parked and charging too | DRIVES_ONLY |
DRIVES_ONLY=true is the default and worth keeping: a parked car keeps producing
near-identical positions, which bloats Dawarich and skews its visit detection.
INITIAL_LOOKBACK: 0The first pass starts at the current time, so only new drives arrive.
INITIAL_LOOKBACK=24h. Anything driven in the last 24 hours is picked up on the
first pass, everything older is ignored.
Stop the service first: both containers share /data and would overwrite each
other's cursor.
docker compose stop teslamate-dawarich
docker compose run --rm teslamate-dawarich -from 2024-01-01 -once
docker compose up -d teslamate-dawarichSwap -from 2024-01-01 for -full to take everything TeslaMate ever recorded.
Both rewrite the cursor, so the service carries on from where the backfill ended.
Re-running either is harmless — Dawarich deduplicates, so nothing doubles.
Expect roughly a thousand points per second, so a million positions is about a quarter of an hour. Progress is logged every 10,000 points:
level=INFO msg="sync in progress" points=590000 through=2025-02-05T15:21:54Z
The cursor is saved after every accepted batch, so an interrupted run resumes where it stopped rather than starting over.
Dawarich is the slower half. Each batch queues anomaly detection, track and visit
recalculation, and those jobs keep running long after the last point arrived —
that queue, not the import, decides when the map is complete. Watch it at
/sidekiq and consider raising BACKGROUND_PROCESSING_CONCURRENCY (default 3)
for the duration.
Delete that import in Dawarich first. Deleting an import removes its points, and
that avoids two slightly different copies of the same drive: GPX rounds
coordinates and timestamps, so those points do not deduplicate against these
ones. Keep the GPX only if it covers a period TeslaMate never recorded, and then
start the backfill after that date with -from.
docker compose logs --tail 50 teslamate-dawarichA pass that sent something logs sync pass complete points=N. Silence is not a
failure: with the defaults the first pass only covers the last 24 hours of
drives, so a day without driving has nothing to send. To see every query, run
one pass verbosely:
docker compose run --rm -e LOG_LEVEL=debug teslamate-dawarich -onceThe cursor shows how far the sync has got:
docker compose exec teslamate-dawarich cat /data/bookmark.jsonDawarich itself is the final word. Points carry the car name as tracker_id,
which distinguishes them from a phone or an earlier GPX import:
curl -s "https://dawarich.example.com/api/v1/points?api_key=KEY&start_at=2026-08-01&end_at=2026-08-19&per_page=5&order=desc"start_at and end_at accept an ISO date or a Unix timestamp, and the response
headers carry X-Total-Pages. On the map, a drive appears as a track once
Sidekiq has processed the new points, a few seconds behind the insert.
To confirm a backfill covered everything, compare its points=N with what
TeslaMate holds:
docker compose exec -T database psql -U teslamate -d teslamate -c \
"SELECT count(*), min(date), max(date) FROM positions
WHERE latitude IS NOT NULL AND longitude IS NOT NULL AND drive_id IS NOT NULL;"N runs a few percent above that count, because each page re-reads the overlap
window and Dawarich discards what it already has. N below the count means
something did not arrive.
| Variable | Default | Meaning |
|---|---|---|
DAWARICH_URL |
— | Base URL of your Dawarich instance |
DAWARICH_API_KEY |
— | Dawarich API key |
DAWARICH_FORWARDED_PROTO |
— | Set to https when reaching a force_ssl instance over internal HTTP |
TESLAMATE_DB_URL |
— | TeslaMate Postgres connection string |
POLL_INTERVAL |
15s |
Upper bound on sync latency |
OVERLAP_WINDOW |
5m |
How far behind the cursor each pass re-reads |
INITIAL_LOOKBACK |
24h |
Where the first run starts with no cursor |
BATCH_SIZE |
1000 |
Points per Dawarich request |
DRIVES_ONLY |
true |
false also syncs parked and charging positions |
CAR_IDS |
all | Comma-separated TeslaMate car ids |
TRACKER_PREFIX |
— | Prepended to the car name to form Dawarich's tracker_id |
MQTT_HOST |
— | Set to enable nudging; unset disables MQTT entirely |
MQTT_PORT |
1883 |
8883 when MQTT_TLS=true and no port is given |
MQTT_USERNAME / MQTT_PASSWORD |
— | Broker credentials |
MQTT_CLIENT_ID |
teslamate-dawarich |
Broker client id |
MQTT_TLS |
false |
TLS to the broker |
MQTT_NAMESPACE |
— | TeslaMate's MQTT_NAMESPACE, if you set one |
LOG_LEVEL |
info |
debug, info, warn, error |
DRIVES_ONLY=true is the default because a parked car keeps producing near
identical positions, which bloats Dawarich and skews its visit detection.
| Flag | Effect |
|---|---|
-once |
Sync everything pending, then exit (useful from cron) |
-from <time> |
Move the cursor to an RFC3339 timestamp or YYYY-MM-DD |
-full |
Move the cursor to the beginning of time |
-version |
Print the version |
| Dawarich | TeslaMate |
|---|---|
| coordinates | latitude, longitude |
timestamp |
date |
altitude |
elevation |
velocity |
speed, converted from km/h to m/s |
battery |
battery_level |
battery_status |
charging when power is negative, else unplugged |
tracker_id |
TRACKER_PREFIX + car name |
Images are published to Docker Hub as lukstankovic/teslamate-dawarich and to
ghcr.io/lukstankovic/teslamate-dawarich.
make test # go test -race ./...
make lint # golangci-lint
make buildTagging is the trigger. make version prints the next patch, minor and major
version; make release-patch, -minor or -major runs the tests, tags, pushes,
writes the GitHub release and appends its notes to CHANGELOG.md. The tag starts
the release workflow, which builds the multi-arch image and pushes it to Docker
Hub and ghcr — around six minutes, most of it arm64 under emulation.
The workflow needs DOCKERHUB_USERNAME and DOCKERHUB_TOKEN as repository
secrets. The Docker Hub repository does not need to exist beforehand; the first
push creates it.
MIT