- MongoDB replaced by InfluxDB 2 (one bucket, installationId tag). Consumption is stored per chunk at its recordedAt and summed on read, so a chunk the car re-sends from its cache overwrites itself instead of being counted twice ($inc did count it twice). Lifetime is a running total derived on read. - POST /v1/telemetry takes batches of samples in ABRP's tlm fields; every field is bounded, unknown fields are dropped, unread signals stay absent. - After a successful write the newest fresh sample is forwarded to ABRP with ABRP_API_KEY / ABRP_USER_TOKEN from the server environment; rate limited, never queued. The car no longer needs ABRP credentials. - Self-hosting: standalone output, Dockerfile (tests run in the build, runs as UID 10001), .env.example and README for the new setup. - Dev shell gains Node 22 and InfluxDB 2. Verified locally against InfluxDB 2.7.12: all endpoints, retry idempotency, and the dashboard pages. Unit tests: 17 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.5 KiB
Nils' MG4 — car server
Next.js app, self-hosted at car.stinnesbeck.com: the ingest API the car writes to, the ABRP forwarder, and a phone-shaped dashboard for reading the history back. Data lives in InfluxDB 2. Based on upstream's Vercel/MongoDB app in FatihsMG4.
car ──live telemetry───▶ server ──▶ ABRP api.iternio.com/1/tlm/send
──daily chunks─────▶ │
──charging sessions▶ ▼
InfluxDB 2
Credentials
| Who holds it | What it opens | |
|---|---|---|
INGEST_TOKEN |
the car, entered in the app | the write endpoints, and the car's own history read |
DASHBOARD_PASSWORD |
you, in a browser | the dashboard and the GET /v1/history reads |
ABRP_API_KEY, ABRP_USER_TOKEN |
only the server | forwarding live telemetry to ABRP |
The ABRP credentials never reach the car. A missing or empty one switches forwarding off.
Environment variables
See .env.example. In Kubernetes they come from the Secret in ../k8s/.
| Name | Notes |
|---|---|
INFLUX_URL, INFLUX_TOKEN, INFLUX_ORG, INFLUX_BUCKET |
bucket defaults to mg4; the token needs read/write on it |
INGEST_TOKEN |
at least 24 characters, or every write is refused: openssl rand -hex 32 |
DASHBOARD_PASSWORD |
at least 8 characters, or login is refused outright |
SESSION_SECRET |
openssl rand -hex 32; rotating it signs every browser out |
ABRP_API_KEY, ABRP_USER_TOKEN |
optional; both needed to forward |
ABRP_MIN_INTERVAL_SECONDS |
default 5 |
Endpoints
Write, Authorization: Bearer <INGEST_TOKEN>:
POST /v1/telemetry—{ installationId, samples: [...] }, up to 500 samples in ABRP'stlmfield names and units (utcin epoch seconds; flags as 0/1 or booleans). Stored, then the newest sample is forwarded to ABRP if it is under 5 minutes old.PUT /v1/consumption/daily— one delta chunk of a day's consumption.PUT /v1/charging-sessions/:sessionId
Read:
GET /v1/consumption/history/:installationId— ingest token; the car reads its own totals back.GET /v1/history/:installationId/consumption,.../charging— session cookie orAuthorization: Bearer <DASHBOARD_PASSWORD>.
GET /v1/health is unauthenticated and returns {"status":"ok"}.
Storage
One bucket, installationId tag on every point:
| Measurement | Timestamp | Tags | Fields |
|---|---|---|---|
telemetry |
sample utc |
— | the ABRP fields that were sent |
consumption_chunk |
recordedAt |
date |
km, kwh, hours, socDrop, sohPercent |
charging_session |
startedAt |
sessionId |
endedAtMs, duration, energy, cost, start/end SOC |
charging_curve |
point time | sessionId |
powerKw, socPercent |
A point with the same measurement, tags and timestamp replaces the earlier one, so the car's
retries are idempotent: a cached chunk is re-sent with its original recordedAt. Days and the
lifetime total are summed from the chunks when read. All numbers are written as floats, because
InfluxDB fixes a field's type on first write.
Local development
nix develop # from the repository root: Node 22 and InfluxDB 2
cp .env.example .env.local # Next loads .env.local automatically
npm install
npm run dev
npm test # validation, aggregation and forwarding rules; no database
Container image
podman build -t git.stinnesbeck.com/nils/mg4-server:<version> .
podman push git.stinnesbeck.com/nils/mg4-server:<version>
The build runs the unit tests. The image runs as UID 10001 and listens on port 3000.