# DataMetric360 — companion app Private vehicle app for one car. Ships three ways from one codebase: native iOS and Android via Capacitor, and embedded as a plain iframe in the Home Assistant dashboard. Architecture and the decisions behind it: [`../COMPANION_APP_ARCHITECTURE.md`](../COMPANION_APP_ARCHITECTURE.md). ## Status **Data layer only.** No UI yet — the screens come from the Claude Design draft ([`../DESIGN_BRIEF_DATAMETRIC360.md`](../DESIGN_BRIEF_DATAMETRIC360.md)), and get implemented on top of `design-system/`'s React components once that draft settles. This package was written first on purpose: how the app talks to Home Assistant doesn't depend on what the screens look like, so it survives every design iteration untouched. Not yet added (deliberately, they'd be guesses today): React/Vite, Capacitor, the native secure-storage adapter, and the Audi brand assets (fonts/rings/badges — those come from `homeassistant/www/` at implementation time, never into `design-system/`, see the licence note in the architecture doc). ## Layout ``` src/api/ ├── types.ts HA state shapes + domain types (Fahrt, Tankvorgang, …) + the entity-ID table ├── umgebung.ts runtime detection (capacitor/iframe/browser), credential storage, URL helpers ├── rest.ts REST client — replaces hass.states / hass.callService ├── live.ts WebSocket client — push updates, auto-reconnect with backoff ├── warteschlange.ts offline queue for writes made without a connection └── index.ts DataMetricApi — ties the three together, exposes the domain operations scripts/smoke.ts verification against a running HA instance ``` Comments are in German, matching the rest of the project. ## What replaces what The old panel got a `hass` object injected by `panel_custom`. That object only exists inside the HA frontend, which is exactly why the old panel can't run as a standalone app. The mapping: | Old panel | Here | |---|---| | `hass.states[id].attributes.daten` | `rest.datenLesen(id)` / `DataMetricApi.profilLesen()` etc. | | `hass.callService(...)` | `warteschlange.einreihen(...)` via the `DataMetricApi` methods | | automatic re-render on state push | `live.aufZustand(...)` | Same entities, same pyscript services, same backend files written — only the transport changes. Every **write** goes through the queue rather than straight to REST. That's what makes offline edits behave the same as online ones, just delayed: a receipt photographed in a dead zone is persisted and sent when the connection returns, surviving an app restart in between. ## Commands Node is installed at `C:\Program Files\nodejs` but is **not on PATH** — prefix it: ```bash $env:Path = "C:\Program Files\nodejs;" + $env:Path ``` ```bash npm run typecheck ``` ```bash npm run smoke ``` `smoke` targets `http://localhost:18123` (the `audi_ha_test` Docker container) by default; pass a different base URL as the first argument. ## Verification status (2026-08-10) `tsc --noEmit` clean under `strict` plus `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. All 7 smoke checks pass against the running test container, covering URL normalisation, the http→ws/https→wss mapping, that `GET /api/` exists and rejects an unauthenticated request with 401, and that the WebSocket opens with `auth_required` — which is the message `live.ts`'s whole auth flow is built around. **Not yet verified — needs a token:** authenticated reads, service calls, and the queue's real round-trip. Those need a Long-Lived Access Token, which is a deliberate manual step (see the LLAT provisioning decision in the architecture doc). To do it: create a token in the HA profile page, then extend `scripts/smoke.ts` with authenticated cases.