# 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 `: - `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 `. `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 ```sh 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 ```sh podman build -t git.stinnesbeck.com/nils/mg4-server: . podman push git.stinnesbeck.com/nils/mg4-server: ``` The build runs the unit tests. The image runs as UID 10001 and listens on port 3000.