Files
mg4/web
nilsandClaude Opus 5.5 4ea66cf424 Server: InfluxDB 2 storage, live telemetry endpoint, ABRP forwarder
- 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>
2026-10-17 14:09:29 +02:00
..

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's tlm field names and units (utc in 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 or Authorization: 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.