docs/teslamate-api-integration-research.md
Research date: 2026-09-02
Source snapshots:
TeslaMate itself does not provide a historical drives/positions JSON API. Its only native JSON routes resume or suspend logging; it also has a GPX download for one already-known drive ID. The TeslaMate source is explicit about those routes, and the GPX controller provides no drive enumeration endpoint (router, GPX controller). Tesla's own API is not a substitute: TeslaMate's official FAQ says it does not provide historical drives or charges (FAQ).
The realistic HTTP integration target is the separate, MIT-licensed community service tobiasehlert/teslamateapi. It reads TeslaMate's PostgreSQL database and MQTT broker and exposes the collected data as JSON. It is not maintained under teslamate-org, has no OpenAPI document, and its README says fuller endpoint documentation is still to come (README). Therefore Dawarich should describe the integration as TeslaMateApi-compatible, not as an official TeslaMate API.
Recommended flow:
GET /api/v1/cars to discover vehicle IDs.GET /api/v1/cars/:car_id/drives?page=N&show=100.drive_id, fetch GET /api/v1/cars/:car_id/drives/:drive_id.data.drive.drive_details, converting time and speed as described below.This imports positions belonging to completed drives only. The list query requires a non-null drive end_date, and the detail query only selects positions with the requested drive_id (list query, detail query). It omits the current unfinished drive and TeslaMate positions recorded while parked/online with no drive. Direct PostgreSQL access is the only complete historical-position surface.
| Surface | Historical positions | Contract and suitability |
|---|---|---|
| TeslaMate core HTTP | One known drive as GPX | The GPX contains latitude, longitude, optional elevation, and ISO-8601 time, ordered by time (template). There is no HTTP drive list, so this cannot power a standalone full import. |
| TeslaMate MQTT | No history | MQTT publishes current/last vehicle state, location, speed, heading, and elevation for automation clients (official MQTT docs). It is useful for future live ingestion, not a historical backfill. |
| TeslaMateApi HTTP | Completed-drive positions | Best fit for a Dawarich user-facing integration. It discovers cars, lists drives, and returns the positions nested in each drive detail (routes). |
| Direct TeslaMate PostgreSQL | All positions | Complete and efficient, including rows without a drive. TeslaMate's official deployment already gives Grafana direct DB credentials (Docker docs), but the schema is an internal contract and DB access is usually not exposed outside the Docker network. Keep this as a future/advanced adapter, not the first implementation. |
TeslaMateApi normally runs on port 8080 beside TeslaMate and requires TeslaMate DB credentials, TZ, and (unless disabled) MQTT configuration. The project's compose example and environment table are the authoritative setup reference (compose example, environment variables). For Dawarich the user-facing configuration should be:
http://teslamateapi:8080 or https://teslamate.example.com.The important security trap is that canonical v1.25.0 does not authenticate read endpoints, including cars and drives. API_TOKEN validation is only called by command/logging handlers, and the README only requires it for those mutating endpoints (authentication docs, auth implementation). The project explicitly recommends authentication in front of the container and demonstrates Traefik Basic Auth (security guidance, Traefik example). Dawarich must never imply that filling an API-token field secures an otherwise exposed TeslaMateApi instance.
For connection testing, request GET /api/v1/cars. /api/ping and /api/healthz only prove that the process is alive, not that a readable TeslaMate database is available; readyz is tied to MQTT readiness unless MQTT is disabled (health handlers). Every response includes an API-Version header, available since TeslaMateApi 1.23.0 (header middleware).
GET /api/v1/cars is unpaginated and returns:
{
"data": {
"cars": [
{
"car_id": 1,
"name": "My Tesla",
"car_details": {
"eid": 123,
"vid": 456,
"vin": "...",
"model": "3",
"trim_badging": "...",
"efficiency": 0.153
},
"car_exterior": { "exterior_color": "...", "spoiler_type": "...", "wheel_type": "..." },
"car_settings": { "suspend_min": 21, "suspend_after_idle_min": 15, "req_not_unlocked": false, "free_supercharging": false, "use_streaming_api": true },
"teslamate_details": { "inserted_at": "...", "updated_at": "..." },
"teslamate_stats": { "total_charges": 1, "total_drives": 2, "total_updates": 3 }
}
]
}
}
The structs and SQL query are defined directly in the handler (cars handler). Dawarich only needs car_id and a display name. An empty result serializes cars as null, not [], because the Go slice starts nil (response construction).
GET /api/v1/cars/:car_id/drives accepts:
page: one-based page, default 1.show: page size, default 100.startDate: include drives whose start_date >= this instant.endDate: include drives whose end_date <= this instant.minDistance and maxDistance: in the TeslaMate user's configured length unit.location: case-insensitive substring of the resolved start or end location.The public README documents the date/distance/location filters (README); page and show are implemented but not documented there. Their defaults and offset calculation are in the handler (pagination code, offset calculation). There is no enforced maximum page size, total count, next-page link, or pagination metadata. Rows are ordered newest-first by start_date; date filter semantics and LIMIT/OFFSET are visible in the SQL builder (filters/order).
The response shape is:
{
"data": {
"car": { "car_id": 1, "car_name": "My Tesla" },
"drives": [
{
"drive_id": 42,
"start_date": "2026-08-20T10:00:00+02:00",
"end_date": "2026-08-20T10:30:00+02:00",
"start_address": "...",
"end_address": "...",
"odometer_details": { "odometer_start": 1000.0, "odometer_end": 1020.0, "odometer_distance": 20.0 },
"duration_min": 30,
"duration_str": "00:30",
"speed_max": 100,
"speed_avg": 40.0,
"power_max": 120,
"power_min": -30,
"battery_details": { "start_usable_battery_level": 80, "start_battery_level": 81, "end_usable_battery_level": 74, "end_battery_level": 75, "reduced_range": true, "is_sufficiently_precise": true },
"range_ideal": { "start_range": 400.0, "end_range": 370.0, "range_diff": 30.0 },
"range_rated": { "start_range": 380.0, "end_range": 350.0, "range_diff": 30.0 },
"outside_temp_avg": 20.5,
"inside_temp_avg": 21.0,
"energy_consumed_net": 4.5,
"consumption_net": 225.0
}
],
"units": { "unit_of_length": "km", "unit_of_temperature": "C" }
}
}
The complete list struct is the de facto schema (drive list structs). When a page has no rows, data.drives is null and the unit/name strings are empty because all are populated while scanning rows (response construction). Treat both null and [] as end-of-pagination.
GET /api/v1/cars/:car_id/drives/:drive_id returns the same summary as data.drive plus unpaginated data.drive.drive_details. Each detail element has this schema (detail structs):
{
"detail_id": 12345,
"date": "2026-08-20T10:00:02+02:00",
"latitude": 52.5201,
"longitude": 13.4051,
"speed": 42,
"power": 8,
"odometer": 1000.1,
"battery_level": 81,
"usable_battery_level": 80,
"elevation": 34,
"climate_info": {
"inside_temp": 21.0,
"outside_temp": 20.5,
"is_climate_on": false,
"fan_status": 0,
"driver_temp_setting": 20.0,
"passenger_temp_setting": 20.0,
"is_rear_defroster_on": false,
"is_front_defroster_on": false
},
"battery_info": {
"est_battery_range": 370.0,
"ideal_battery_range": 390.0,
"rated_battery_range": 375.0,
"battery_heater": false,
"battery_heater_on": false,
"battery_heater_no_power": false
}
}
Nullable database values in usable_battery_level, elevation, climate fields, and battery-info fields serialize as JSON null (null wrappers). Positions are ordered by their database ID ascending, which normally matches capture order; Dawarich should still order/dedupe by parsed timestamp during insertion (position query).
TZ, but Dawarich should always send RFC3339 with Z to avoid ambiguity (date parser).TZ configured on TeslaMateApi. Parse them as instants and store Unix seconds in Dawarich; never strip the offset (time conversion).unit_of_length == "km" and converts it to mph when the setting is "mi" (detail conversion). Dawarich velocity is conventionally m/s, so convert with km/h / 3.6 or mph * 0.44704.elevation is meters above sea level and is not converted for imperial users; TeslaMate's official MQTT contract confirms the unit (MQTT docs).usable_battery_level when present, otherwise battery_level.Suggested Dawarich mapping:
| TeslaMateApi | Dawarich |
|---|---|
longitude, latitude | lonlat = POINT(longitude latitude) |
date | RFC3339 parse, then Unix seconds timestamp |
elevation | altitude / altitude_decimal, meters |
speed + data.units.unit_of_length | velocity, converted to m/s |
| `usable_battery_level | |
car_id | stable tracker ID such as teslamate-<car_id> |
drive_id, detail_id, source fields | raw_data for provenance/debugging |
TeslaMateApi has nonstandard error semantics. Database errors, invalid date filters, and a missing drive all go through TeslaMateAPIHandleErrorResponse, which returns HTTP 200 with a root JSON object such as {"error":"Unable to load drives."}, {"error":"Invalid date format."}, or {"error":"No rows were returned!"} (error handler, missing-drive branch). Unknown routes are conventional HTTP 404 JSON (router); a reverse proxy may also return ordinary 401/403 or non-JSON bodies.
The client must therefore:
data structure.error key even when the status is 200.drives: null/cars: null as an empty success, not an exception.Malformed numeric URL/query values are converted silently to zero. Invalid car_id, page, or show therefore do not reliably produce a 400 response (conversion helpers). Validate them within Dawarich.
For an initial import:
sync_cutoff = Time.current.utc once and send it as endDate. This prevents a newly completed drive from being inserted at the front while offset pagination is in progress.show (100 is the implementation default).drives is null/empty or the returned count is smaller than show.(user_id, timestamp, lonlat) and retain TeslaMate car_id, drive_id, and detail_id in provenance data.For later syncs, use an overlapping startDate window and a new fixed endDate, then rely on idempotent upserts. A small overlap is preferable to a strict last-timestamp cursor because the list is newest-first offset pagination and TeslaMateApi exposes neither a cursor nor a total count. Note that its filters require start_date >= startDate and end_date <= endDate, so a drive crossing a boundary is excluded; overlap avoids losing it (filter implementation).
If complete location history becomes a requirement, add a separately named PostgreSQL adapter rather than silently changing the TeslaMateApi integration. The authoritative TeslaMate schema has a nullable drive_id relationship and the required WGS84/time fields plus optional speed, elevation, odometer, and battery data (position schema). A scalable read shape is an ID cursor:
SELECT id, car_id, drive_id, date, latitude, longitude,
elevation, speed, battery_level, usable_battery_level
FROM positions
WHERE id > $1
ORDER BY id ASC
LIMIT $2;
Use a dedicated read-only PostgreSQL role, TLS where the DB crosses hosts, and persist the last completed ID. This avoids the HTTP N+1 pattern and includes positions outside drives, but it deliberately accepts coupling to TeslaMate migrations and requires more sensitive credentials. TeslaMateApi itself illustrates that coupling by querying the same cars, drives, positions, and settings tables directly (drive SQL).
API_TOKEN with Tesla Fleet/Owner API credentials. Dawarich only reads already-collected history and should never request Tesla account tokens.API-Version response header.