Files
audi-app/COMPANION_APP_ARCHITECTURE.md
T
tobias e1d570992e Initialer Import: HA-Panel, Design-System, Companion-App
Drei zusammengehörige Teile in einem Repository:

- homeassistant/  Das fertige, im Einsatz befindliche Home-Assistant-Panel
  (panel_custom Custom Element + pyscript-Backend). Echte Fahrzeug- und
  Personendaten (fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl,
  Tankbelege) bleiben per .gitignore außen vor; die anonymisierte Vorlage
  fahrzeugprofil.example.json ist mit dabei.

- design-system/  Eigenständige React-Komponentenbibliothek (@audi-dash/ui),
  die die visuelle Sprache des Panels nachbildet - ohne Audi-Markenzeichen
  und ohne die lizenzierte Hausschrift. Dient als Grundlage für Claude
  Design. War bis hierher ein eigenes Repository und ist in dieses
  eingeschmolzen worden.

- companion-app/  Datenschicht der neuen App DataMetric360 (iOS/Android via
  Capacitor, zusätzlich als Iframe im HA-Dashboard). Noch ohne Oberfläche:
  REST- und WebSocket-Zugriff auf Home Assistant plus Warteschlange für
  Änderungen ohne Netz. Ersetzt das eingespritzte hass-Objekt, das nur
  innerhalb des HA-Frontends existiert.

Dazu die Projektdokumentation: SPECIFICATION.md (Ist-Stand des Panels),
COMPANION_APP_ARCHITECTURE.md (Architekturentscheidungen der neuen App),
AUDIT_2026-08-10.md, DESIGN_BRIEF_DATAMETRIC360.md und der ursprüngliche
Bauauftrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 00:20:05 +02:00

18 KiB

DataMetric360 — Companion App Architecture (design decided, not yet built)

App name: DataMetric360 (decided 2026-08-10). Public hostname: a subdomain of datametric360.app, a dedicated domain registered at all-inkl solely for this purpose (see §5.4 for why it must be a separate domain). Both the app name and the domain are deliberately neutral — they don't advertise "Audi", "car", or "GPS tracker" to anyone who sees the hostname or the app icon, which is a small but free privacy win for a hostname that will be publicly resolvable.

Status: architecture decisions locked in during design discussion on 2026-08-10. No code for this exists yet — this is a separate, not-yet-started project sitting alongside the finished HA panel described in SPECIFICATION.md. Implementation is explicitly deferred until the FMM003 hardware is installed and wired (owner's own framing: "Inbetriebnahme findet separat statt, wenn die HW vorrätig ist"). This file exists so the decisions already made aren't lost before that point.

Relationship to the rest of the repo:

  • homeassistant/ — the finished, in-use HA panel (SPECIFICATION.md is authoritative for it). Its data source today is the HACS integration TommiG1/HA_VAG-EU-Data-Act plus the iPhone Companion App's WLAN sensor for trip detection.
  • design-system/ — the React component library (no Audi brand assets) built to feed Claude Design. Becomes directly useful here: it's the intended shared UI foundation for the app described below.
  • This file — describes a new, third piece: a companion app that will eventually supersede the HA panel's role, once built.

1. Concept

One responsive web app, built with the design-system/ component library, shipped three ways:

  1. Native iOS app — via Capacitor.
  2. Native Android app — via Capacitor.
  3. Embedded in Home Assistant — as a plain panel_iframe (not panel_custom/hass injection), so the same build just gets more screen space and reflows via CSS breakpoints instead of floating a fixed phone-card layout in the middle of a bigger screen.

Once this exists, it supersedes homeassistant/www/audi-dashboard-app.js — the current panel_custom implementation is not meant to be maintained forever in parallel; it gets replaced, not duplicated. That's an explicit, confirmed decision, not an assumption. audi-dashboard-app.js gets archived, not deleted, once superseded — same safety-net pattern already used in this project (.bak-before-audit-merge files).

Distribution: sideload only, no App Store / Play Store. Confirmed decision. This matters beyond convenience: the Audi Type font, the four-rings SVG, and the model-badge SVGs are licensed only for this one private, non-published installation (SPECIFICATION.md §2, bauauftrag.md §8/§12) — exactly why design-system/ strips all of that out, since it gets uploaded to a third party (Claude Design). A sideloaded app installed directly onto the owner's own two phones, never published to a store, stays inside that same private-installation scope — so the companion app can use the real Audi assets, matching the "same look and feel" decision already made, no generic-fallback treatment needed.

Frontend head start via design-system/ + Claude Design: the component-library sync gave Claude Design the real building blocks (tokens, ~19 components) to design with — it has not yet designed this app's actual screens (Home, Trips, Fuel, Settings, reworked around the REST/WebSocket data model instead of hass). That's a future step, following the same pattern as the original Design-Audit task this session started with: design in Claude Design using the real components, then implement the result into actual Capacitor/React code.

The iPhone stops being a data source entirely — it becomes purely a display device for this app, same as Android. All vehicle data comes from the FMM003 (see §2), not from the iPhone Companion App.


2. Data source: FMM003 replaces the iPhone + VAG integration

Confirmed, explicit decision (retiring two existing data sources):

  • The iPhone Companion App's WLAN sensor (sensor.iphone_wifi_connection), used today for trip start/end detection, is fully retired. No fallback to it.
  • The HACS integration TommiG1/HA_VAG-EU-Data-Act is fully retired as a data source.
  • All vehicle data comes from the FMM003 (fully wired to the vehicle's CAN/FMS bus), via Codec8, into Home Assistant as one combined, flexibly-mapped entity (no hardcoded field list — see the no-hardcoding mapping system discussed separately, not yet written to a file).

Decided (2026-08-10, after web research — see rationale below): Traccar, minimal variant.

Research question: is there something more efficient than Traccar for decoding Codec8 from an FMM003? Checked and ruled out:

  • HA's official "Teltonika" integration — for Teltonika routers (RUT series: signal/modem/network monitoring), unrelated to GPS/AVL trackers. Not applicable.
  • A Teltonika-RMS-based HA integration exists (derliebemarcus/teltonika_rms) but connects through Teltonika's own cloud (RMS) — same objection as the earlier Flespi rejection: vehicle/GPS data would transit an external vendor cloud, which is exactly what this whole redesign was meant to avoid.
  • OpenRemote (an alternative fleet platform some Teltonika docs point to) — a full IoT asset-management platform with a rule engine and dashboards; heavier than Traccar, not lighter.
  • Minimal standalone Codec8 parser libraries (e.g. teltonika-go, small Python GPS-server scripts) — genuinely lighter in principle (no bundled platform/Tomcat), but sacrifice exactly what makes Traccar the safer choice: even Traccar itself, mature and community-maintained for years, still has open issues about firmware-specific Codec8 decoding quirks per device generation. A standalone parser would hit the same class of edge cases with none of that accumulated fixing behind it.

Conclusion: no better alternative found — Traccar stays, but in a genuinely minimal shape:

  • Database: H2 (embedded, file-based), not MariaDB. This was a wrong assumption in the first draft of this document — Traccar's own upstream default database is H2, and the HA Community Add-on (hassio-addons/addon-traccar) ships with H2 pre-configured (jdbc:h2:/data/traccar) out of the box. MariaDB is only the add-on docs' recommendation for larger/multi-user production deployments, not a hard requirement. For one vehicle, one device, one private user, H2 is the right call — no MariaDB add-on needed at all, one fewer service to run and back up.
    • Known trade-off (from community reports): occasional H2 corruption/data loss across Traccar version upgrades. Mitigation: extend the existing daily backup.py routine to also copy Traccar's .mv.db file alongside the other data files — same backup pattern already in place, no new mechanism.
  • Disable geofencing and reports/notifications in Traccar's config — none of that applies to a single always-known vehicle; keeps the running feature surface to just device ingestion + Position Forwarding, which is all this project actually uses.
  • Position Forwarding → HA webhook (as already planned) remains the integration path for CAN/FMS attributes (fuel, RPM, etc.), since HA's own official traccar_server integration is device_tracker/geofence-focused and wouldn't surface those extra AVL fields on its own.

Hardware confirmed sufficient for this: Dell OptiPlex 3000 TC, Pentium N6005, 16 GB DDR4, 256 GB NVMe — comfortably enough for Traccar alone, more so without a MariaDB add-on alongside it.

Trip detection (substitutes the old WLAN-based logic)

  • Start / end trigger: FMM003 ignition state (CAN), not WLAN connectivity.
  • Pause-tolerance behavior carries over unchanged from the existing design — this was iterated three times during design and is settled:
    • Ignition never drops during engine start-stop (confirmed technical fact), so no separate hold-time is needed to filter start-stop out — the ignition: 1→0 condition already excludes it.
    • The existing user-configurable pause-tolerance setting (fahrten_pausenzeit_min, today's default 15 min, editable in Settings) is a real, intentional product feature and must be preserved: if the vehicle restarts within that window after a genuine ignition-off, it's one trip, not two.
    • Implementation pattern (unchanged from the current fahrterkennung.py approach, just re-triggered off the FMM003 ignition entity instead of the WLAN sensor): task.unique() cancels a pending task.sleep() if ignition comes back on within the pause window; the trip's recorded end timestamp/odometer is backdated to the actual ignition-off moment, not the timer-expiry moment.
  • sensor.iphone_wifi_connection drops out of pyscript/modules/einstellungen.py entirely once this ships.

3. Frontend data access: HA REST/WebSocket API, not hass injection

The new app is not a panel_custom element, so it has no injected hass object. Instead:

  • Auth: a Home Assistant Long-Lived Access Token (LLAT), generated once per install in the HA user profile. Sufficient for a private single-user install — no need for the full OAuth flow the official Companion App uses.
  • Reads: GET /api/states/<entity_id> per entity, or subscribe over HA's WebSocket API for push updates instead of polling.
  • Writes: POST /api/services/<domain>/<service> — calling the same pyscript.audi_dashboard_* services the current panel already calls via hass.callService() (e.g. audi_dashboard_profil_schreiben, audi_dashboard_beleg_hochladen). Same backend function, same file written server-side — a receipt upload or a FIN/plate edit from the phone is a real round-trip to the shared dataset, never a local-only change. The phone only caches for offline viewing and holds the token; it is never the source of truth.
  • This same REST/WebSocket client is what makes the iframe-in-HA case (§1.3) work identically to the native apps — one data-access layer for all three targets, no hass-injection special-casing needed for the iframe case.

LLAT provisioning (decided)

Primary: manual paste into a one-time setup screen, saved to secure on-device storage (iOS Keychain / Android Keystore, via a Capacitor secure-storage plugin). Always available, no permissions, no extra infrastructure — and the guaranteed fallback if anything about the QR path misbehaves.

Secondary (preferred by the owner, build if it stays simple): QR code scan. The one real objection to QR — that generating it would mean pasting a long-lived token into some third-party online QR generator — is avoidable: generate the QR locally, in a small self-hosted page under HA's /local/, using an offline JS QR library, rendering entirely client-side with no network call. The token then never leaves the local network on its way into the QR. On the app side this needs a Capacitor barcode/camera plugin and the camera permission. Both halves are small and well-trodden; if the implementation turns out to be more than a modest amount of work, ship manual paste alone — it's fully sufficient on its own.

A URL/deep-link bootstrap was considered and rejected: it needs a registered custom URL scheme plus some channel to send yourself the link, which exposes the token in transit at least as much as pasting it, for no gain over the two options above.

Offline behavior (decided)

  • The app shows the last cached data when it has no route to HA, with a visible "Offline" marker in the header/title area — never a blank screen or silent staleness.
  • All edits made while offline are queued (receipt uploads, profile edits, manual trip/fill-up entries, etc.) and flushed once connectivity returns, rather than rejected outright. Given this is a single-user app, conflict resolution can stay simple (last-write-wins) — there's no concurrent second editor to reconcile against.

4. Connectivity: Option B — narrow gateway, chosen over Tailscale-always-on

Decision: Option B. (Option A — making the phone's Tailscale connection always-on instead of triggered by the car's WLAN SSID — was the lower-effort alternative and remains available if this turns out to be worth revisiting, but Option B is what's being built.)

Goal

The app must be able to read and write data from any network the phone is on (cellular, foreign Wi-Fi, home Wi-Fi) — not just when Tailscale happens to be connected — while Home Assistant itself stays exactly as closed/local-only as it is today: no port-forward, no Funnel, HA's own port/UI/login never reachable from the public internet. Only a narrow, purpose-built slice of the API is ever exposed.

Shape (two existing, maintained HA Community Add-ons — not custom code to maintain)

  1. Cloudflared add-on — runs cloudflared as an HA add-on. Opens an outbound-only encrypted tunnel from the HAOS host (the OptiPlex) to Cloudflare's edge; no router port ever opens. Confirmed capable of routing multiple public hostnames to multiple local ports/services — so it points at the reverse-proxy add-on's port below, not at HA's own port 8123.
  2. Nginx Proxy Manager or Traefik add-on — a path-scoped reverse proxy sitting between the tunnel and HA's internal API, on the Supervisor's internal Docker network. Configured with an allowlist of only the specific paths the companion app needs (e.g. /api/states/sensor.audi_*, /api/services/pyscript/audi_dashboard_* — exact path list still TBD, depends on the final entity/ service names once the FMM003 mapping is built). Everything else — /lovelace, /config, /auth, the HA frontend itself — is blocked at this layer, never reaching the tunnel at all.
  3. Auth stays a second, independent layer: every proxied call still requires the same Bearer LLAT HA's REST API already demands. Even a path that slipped through the allowlist would be useless without that token, and any individual LLAT can be revoked from the HA profile if the phone is ever lost.

Why this over Option A

Feels like a normal app to the user — plain HTTPS, no VPN client running on the phone, no "why is my battery draining" VPN-always-on tradeoff. The cost is two add-ons' worth of configuration (tunnel + path allowlist) instead of a single phone setting, and the honest caveat that a narrow, purpose-built surface is now internet-facing (even though HA itself never is) — accepted as worth it for the UX.

Explicitly not chosen

  • Tailscale Funnel — ruled out earlier for the raw FMM003→decoder TCP ingress path (port/protocol restrictions), and not adopted for this gateway either, since it would mean quietly reversing the existing "no Funnel" rule; not adopted without that being an explicit, separate decision.
  • Direct port-forward — ruled out, contradicts the standing "Home Assistant is never exposed publicly" principle from bauauftrag.md §3, which continues to hold for HA itself under this design.

5. Open items (not yet decided)

Deliberately deferred until hardware arrives / the next architecture review — not blocking anything else in this document:

  1. Exact reverse-proxy path allowlist — depends on the final pyscript entity/service names once the FMM003 combined-entity mapping is implemented; write these down here once decided.

  2. No-hardcoding mapping system for the FMM003 combined entity — discussed in an earlier session, not yet written to a file or finalized in detail.

  3. Reverse-proxy add-on: Nginx Proxy Manager vs. Traefik — both viable (§4), pick deferred to the next audit pass. Doesn't depend on the FMM003 — could be set up and tested against HA's existing API before the hardware arrives, if worth doing ahead of time.

  4. Cloudflare Tunnel domain DECIDED: datametric360.app, being registered fresh at all-inkl for this purpose only (no websites, no mail on it). The tunnel will publish a subdomain of it. Note .app is on the HSTS preload list, so browsers force HTTPS on it unconditionally — which suits a Cloudflare Tunnel (always HTTPS) and removes any chance of an accidental plaintext request.

    The constraint that drove "register a separate domain rather than reuse an existing one":

    Cloudflare Tunnel needs the domain's DNS hosted at Cloudflare — i.e. "Full setup": the domain's nameservers get repointed to Cloudflare, moving all DNS records for that domain (A, MX/email, everything) out of all-inkl's DNS management. The alternative that avoids this — Cloudflare's "partial / CNAME setup", which keeps DNS at the existing provider — is Business-plan-only ($200/month), i.e. not realistic here.

    Therefore: never point this at an existing all-inkl domain carrying live websites or email — hence the dedicated datametric360.app. Nothing about the owner's existing domains/mail is touched.

    Note the all-inkl "Neue Domain anlegen" dialog asks for a target (Webspace / Redirect / Webbaukasten) — that choice is irrelevant here, since DNS gets delegated to Cloudflare afterwards and the all-inkl webspace binding then goes unused. The meaningful step is the nameserver change to Cloudflare, done after registration, not anything in that dialog.


6. Explicit non-goals for this piece (carried over from bauauftrag.md)

  • Home Assistant's own UI/API is never exposed publicly, under either connectivity option.
  • No change to the vehicle being read-only (no remote control) — this app remains a viewer/data-entry tool, same as the existing panel.