Kleinere Befunde und Dokumentation aus dem Review
Frontend, kleinere Befunde: - Das Standort-Menue liess sich nur wischen oder ueber den Kartenmarker oeffnen. Der Griff ist jetzt ein Knopf mit aria-expanded, damit auch per Tastatur erreichbar; der Umschalter Strasse/Satellit hat aria-pressed und ein Label, das den Zustand nennt. - Der Filterschalter "Nur passende Sensoren anzeigen" wirkte nicht, solange eine Auswahlliste offen war: der Klick-Handler ersetzte das Overlay-DOM, bevor das change-Ereignis des Kontrollkaestchens ausgeliefert wurde. Er wird jetzt vor dem Schliessen der Liste behandelt. - Der Theme-Wechsel zeichnete nicht neu, die Leaflet-Kacheln blieben bis zum naechsten Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht deutlich sichtbar. - Toter Code entfernt (fahrzeugGlyphPfade, .dot.neutral) und zwei Kommentare berichtigt, die Gegenteiliges behaupteten. Dokumentation: - AGENTS.md widersprach sich an sechs Stellen. Berichtigt: Fahrterkennung laeuft ueber die Zuendung, nicht ueber WLAN; Datenweg ist flespi, nicht MQTT; Modulzahl und Zeilenzahl stimmen wieder; Entity-IDs werden im Setup-Menue zugeordnet, nicht in einstellungen.py; STANDORT_TRACKER ist belegt, nicht leer. - SPECIFICATION.md beschreibt durchgehend den Stand vor der FMM003- Umstellung. Statt es zu Teilen umzuschreiben und dabei Ungenauigkeiten zu riskieren, steht jetzt ein datierter Hinweis am Anfang, der die drei geaenderten Punkte benennt und auf AGENTS.md verweist. Alles Uebrige des Dokuments gilt unveraendert weiter. - INSTALL.md: Die zirkulaere Schrittfolge (Schritt 4 verweist auf 9, Schritt 7 auf 4) ist als solche benannt und aufgeloest. Die Override-Datei ist mit Pfad genannt, samt dem Hinweis, dass sie gesichert wird. Und die drei mit Test-Entitaeten der Entwicklungsinstanz vorbelegten Rollen sind erwaehnt - auf einer frischen Installation zeigen sie ins Leere. - README, Profilvorlage: WLAN-Reste entfernt, zwei Falschaussagen aus dem ersten Review nachgezogen (Statistik rechnet echt, die 97-Prozent-Volltankungsregel wurde entfernt), Dateiuebersicht um die fuenf fehlenden pyscript-Dateien und entitaeten.py ergaenzt. - update.ps1 liefert jetzt auch shell_beleg_parser.py aus. Die Datei liegt in data/, ist aber Code - Aenderungen am Belegleser, dem einzigen getesteten Teil des Projekts, kamen bisher auf keiner Instanz an. - design/README: Zwei Dateien der Inhaltstabelle liegen gar nicht in dem Ordner, weshalb das Board aus der Repo-Kopie heraus leer bleibt - jetzt vermerkt. Ausserdem die Behauptung berichtigt, die Auflage ruehre audi-dashboard-app.js nicht an: der Stylesheet-Loader wurde dort ergaenzt. Geprueft: Panel laedt, alle neun pyscript-Entitaeten werden veroeffentlicht, Zuordnen und Zuruecksetzen funktionieren, keine neuen Fehler im Protokoll.
This commit is contained in:
@@ -91,14 +91,16 @@ Root files: `dashboard-muster*.html` = original static prototype (superseded, re
|
|||||||
`bauauftrag.md`/`.html` = original build brief (historical), `DESIGN_BRIEF_DATAMETRIC360.md` = the
|
`bauauftrag.md`/`.html` = original build brief (historical), `DESIGN_BRIEF_DATAMETRIC360.md` = the
|
||||||
prompt for the Claude Design project.
|
prompt for the Claude Design project.
|
||||||
|
|
||||||
**`homeassistant/` in one paragraph:** 10 pyscript scripts + 4 modules (`pyscript/modules/`); trip
|
**`homeassistant/` in one paragraph:** 10 pyscript scripts + 5 modules (`pyscript/modules/`); trip
|
||||||
detection via the iPhone WLAN sensor with pause tolerance, two-stage trip completion via HA history
|
detection via the FMM003 ignition sensor with pause tolerance (the iPhone WLAN sensor is gone — see
|
||||||
screening (odometer often updates only on the next trip), fill-up detection on fuel-level rise,
|
section B), two-stage trip completion via HA history screening (odometer often updates only on the
|
||||||
Shell PDF parser (`data/shell_beleg_parser.py`, subprocess, the only tested part of the repo), tire
|
next trip), fill-up detection on fuel-level rise, Shell PDF parser (`data/shell_beleg_parser.py`,
|
||||||
km counter, backup, self-update, image management. Frontend: one file `www/audi-dashboard-app.js`
|
subprocess, the only tested part of the repo), tire km counter, backup, self-update, image
|
||||||
(3,130 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, cache-busting via
|
management. Frontend: one file `www/audi-dashboard-app.js`
|
||||||
`audi-dashboard-version.json` + loader stub. Entity IDs configured centrally in
|
(~4,000 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, cache-busting via
|
||||||
`pyscript/modules/einstellungen.py` (the one file edited before install). Deploy: `update.ps1`
|
`audi-dashboard-version.json` + loader stub. Entity IDs are assigned **in the UI** (Settings →
|
||||||
|
Fahrzeug einrichten → Setup), stored as overrides in `data/entitaeten.json`;
|
||||||
|
`pyscript/modules/einstellungen.py` holds only the built-in defaults. Deploy: `update.ps1`
|
||||||
(robocopy to Samba share) or — still inactive — self-update from git.
|
(robocopy to Samba share) or — still inactive — self-update from git.
|
||||||
|
|
||||||
**`companion-app/` — what exists:** dependency-free, strictly typed TS data layer (`src/api/`):
|
**`companion-app/` — what exists:** dependency-free, strictly typed TS data layer (`src/api/`):
|
||||||
@@ -111,8 +113,10 @@ capacitor/iframe/browser (`umgebung.ts`), types + entity table (`types.ts`), fac
|
|||||||
**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
|
**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
|
||||||
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
|
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
|
||||||
and the VAG integration.
|
and the VAG integration.
|
||||||
- **Current data path (since 2026-08-11): Codec JSON → MQTT/TLS → Mosquitto add-on → HA.** Traccar
|
- **Current data path (since 2026-08-12): FMM003 → flespi (native Codec8/TCP, IMEI auth) → HA.**
|
||||||
is **dropped** (the decision log deliberately remains in the architecture doc, marked ÜBERHOLT).
|
Both earlier directions are dropped: Traccar first, then the self-hosted MQTT/TLS broker — the
|
||||||
|
router's port-forward turned out to be unusable. The decision logs deliberately remain in the
|
||||||
|
architecture doc, marked ÜBERHOLT. Details and open steps in section B below.
|
||||||
- Frontend access: HA REST + WebSocket with a long-lived access token (no `hass` object).
|
- Frontend access: HA REST + WebSocket with a long-lived access token (no `hass` object).
|
||||||
- External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays
|
- External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays
|
||||||
unreachable. Domain **`datametric360.app`** registered (all-inkl, 2026-08-11).
|
unreachable. Domain **`datametric360.app`** registered (all-inkl, 2026-08-11).
|
||||||
@@ -317,9 +321,9 @@ capacitor/iframe/browser (`umgebung.ts`), types + entity table (`types.ts`), fac
|
|||||||
Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API)
|
Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API)
|
||||||
actions. Backend: new `STANDORT_TRACKER` entity-ID setting in `einstellungen.py` (device_
|
actions. Backend: new `STANDORT_TRACKER` entity-ID setting in `einstellungen.py` (device_
|
||||||
tracker with lat/lon attributes), published via a new `_standort()` helper in
|
tracker with lat/lon attributes), published via a new `_standort()` helper in
|
||||||
`frontend_veroeffentlichung.py` — **left empty/unconfigured**, no live vehicle GPS source
|
`frontend_veroeffentlichung.py`. The built-in default points at the author's test device
|
||||||
exists yet (same FMM003/flespi gap as section B); the tile shows "Kein GPS-Signal vom
|
(`device_tracker.testzone_fmm003`); until the FMM003 actually delivers positions (section B),
|
||||||
Fahrzeug" until that's wired up. Verified end-to-end in `audi_ha_test` with a manually
|
that entity does not exist and the tile shows "Kein GPS-Signal vom Fahrzeug". Verified end-to-end in `audi_ha_test` with a manually
|
||||||
created `device_tracker.test_fahrzeug` test entity (tile, fullscreen map, and menu all
|
created `device_tracker.test_fahrzeug` test entity (tile, fullscreen map, and menu all
|
||||||
confirmed rendering/populating correctly); the device's own location (browser Geolocation
|
confirmed rendering/populating correctly); the device's own location (browser Geolocation
|
||||||
API) and the Route/Teilen deep links work independently of the vehicle-GPS gap. Pushed to
|
API) and the Route/Teilen deep links work independently of the vehicle-GPS gap. Pushed to
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# Audi Dashboard — Technical Specification
|
# Audi Dashboard — Technical Specification
|
||||||
|
|
||||||
|
> ## ⚠️ Teilweise überholt (Stand 2026-08-13)
|
||||||
|
>
|
||||||
|
> Dieses Dokument beschreibt den Stand **vor** der FMM003-Umstellung. Drei Dinge haben sich seither
|
||||||
|
> geändert und gelten in diesem Dokument durchgehend als veraltet:
|
||||||
|
>
|
||||||
|
> 1. **Fahrterkennung** läuft über den Zündungssensor des FMM003, nicht mehr über den
|
||||||
|
> iPhone-WLAN-Sensor. `sensor.iphone_wifi_connection` und `fahrzeug.wlan_name` sind entfernt.
|
||||||
|
> 2. **Datenquelle** ist der FMM003 über flespi (Codec8/TCP), nicht mehr die HACS-Integration
|
||||||
|
> `TommiG1/HA_VAG-EU-Data-Act`.
|
||||||
|
> 3. **Entity-IDs** werden im Setup-Menü der App zugeordnet (Einstellungen → Fahrzeug einrichten →
|
||||||
|
> Setup) und in `data/entitaeten.json` abgelegt. `einstellungen.py` enthält nur noch die
|
||||||
|
> eingebauten Vorgabewerte und ist keine Pflicht-Bearbeitung mehr vor der Installation.
|
||||||
|
>
|
||||||
|
> Alles Übrige — Datenmodell, Geschäftsregeln, Frontend-Aufbau, §7 „Known Gaps" — gilt weiterhin.
|
||||||
|
> Den aktuellen Stand führt `AGENTS.md`, Abschnitt B.
|
||||||
|
|
||||||
**Audience:** an AI coding agent picking up this codebase with no prior context.
|
**Audience:** an AI coding agent picking up this codebase with no prior context.
|
||||||
**Scope:** the Home Assistant panel (`homeassistant/`) — the finished, in-use app. A separate sibling project, `design-system/`, is a standalone React component-library replica of this app's visual language, built solely to feed Claude Design (`claude.ai/design`) via the `/design-sync` skill; it is **not** part of this app and deliberately excludes Audi brand assets. `COMPANION_APP_ARCHITECTURE.md` documents a third, **not-yet-built** sibling project — an iOS/Android/HA-iframe companion app fed by a Teltonika FMM003 tracker instead of the WLAN-sensor/VAG-integration data sources described below; its design is decided but no code exists yet. Don't confuse any of the three.
|
**Scope:** the Home Assistant panel (`homeassistant/`) — the finished, in-use app. A separate sibling project, `design-system/`, is a standalone React component-library replica of this app's visual language, built solely to feed Claude Design (`claude.ai/design`) via the `/design-sync` skill; it is **not** part of this app and deliberately excludes Audi brand assets. `COMPANION_APP_ARCHITECTURE.md` documents a third, **not-yet-built** sibling project — an iOS/Android/HA-iframe companion app fed by a Teltonika FMM003 tracker instead of the WLAN-sensor/VAG-integration data sources described below; its design is decided but no code exists yet. Don't confuse any of the three.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,9 +14,9 @@ Das ist der **Entwurf**, nicht der lauffähige Code. Umgesetzt wird er später i
|
|||||||
| `DataMetric360.dc.html` | Einstiegsdatei des Entwurfs |
|
| `DataMetric360.dc.html` | Einstiegsdatei des Entwurfs |
|
||||||
| `DM360.dc.html` | Die eigentlichen Bildschirme und Zustände |
|
| `DM360.dc.html` | Die eigentlichen Bildschirme und Zustände |
|
||||||
| `DataMetric360 Board.dc.html` | Vergleichsansicht: dieselbe App gleichzeitig als Smartphone- und Großbildschirm-Rahmen nebeneinander, mit Bildschirm-/Theme-Umschalter. Steuert beide Rahmen per `postMessage`. |
|
| `DataMetric360 Board.dc.html` | Vergleichsansicht: dieselbe App gleichzeitig als Smartphone- und Großbildschirm-Rahmen nebeneinander, mit Bildschirm-/Theme-Umschalter. Steuert beide Rahmen per `postMessage`. |
|
||||||
| `dm360-host.html` | Lädt `audi-dashboard-app.js` in einem eigenen `<audi-dashboard-panel>`, hängt danach `audi-dashboard-ios.css` in dessen Shadow-Root - der eigentliche Träger hinter dem Board. |
|
| `dm360-host.html` | Lädt `audi-dashboard-app.js` in einem eigenen `<audi-dashboard-panel>`, hängt danach `audi-dashboard-ios.css` in dessen Shadow-Root - der eigentliche Träger hinter dem Board. **Achtung: beide Dateien liegen nicht in diesem Ordner.** Aus der Repo-Kopie heraus bleibt das Board deshalb leer; es läuft nur im Claude-Design-Projekt, wo sie mit hochgeladen sind. |
|
||||||
| `ios-frame.jsx` | iOS-26-Gerätekachel (Statusleiste, Dynamic Island, Home-Indikator) - reine Optik, keine App-Logik. |
|
| `ios-frame.jsx` | iOS-26-Gerätekachel (Statusleiste, Dynamic Island, Home-Indikator) - reine Optik, keine App-Logik. |
|
||||||
| `audi-dashboard-ios.css` | **Die eigentliche Neuerung dieses Exports.** Optik-Auflage über `audi-dashboard.css`: iOS-natives Erscheinungsbild (44-px-Zeilen, transluzente Tab-Leiste, iOS-Schalter) plus - wichtig - die responsive Großbildschirm-Umsetzung per `@container (min-width:860px)` (Tab-Leiste wird zur seitlichen Navigation). **Seit 2026-08-11 in `homeassistant/www/` scharf geschaltet und im Docker-Testcontainer bei 375px und 1280px verifiziert** - rührt `audi-dashboard-app.js` nicht an, wie behauptet. |
|
| `audi-dashboard-ios.css` *(liegt in `../homeassistant/www/`, nicht in diesem Ordner)* | **Die eigentliche Neuerung dieses Exports.** Optik-Auflage über `audi-dashboard.css`: iOS-natives Erscheinungsbild (44-px-Zeilen, transluzente Tab-Leiste, iOS-Schalter) plus - wichtig - die responsive Großbildschirm-Umsetzung per `@container (min-width:860px)` (Tab-Leiste wird zur seitlichen Navigation). **Seit 2026-08-11 in `homeassistant/www/` scharf geschaltet und im Docker-Testcontainer bei 375px und 1280px verifiziert.** Die Auflage selbst ist rein additiv; `audi-dashboard-app.js` wurde nur um den Stylesheet-Loader ergänzt, der sie nachlädt (siehe unten). |
|
||||||
| `datametric360-ios.css` | Dieselbe Optik-Auflage, aber für die `@audi-dash/ui`-Komponentenklassen (`.ads-*`) statt für die Panel-eigenen Klassen - für später, wenn `companion-app/` eine UI bekommt. |
|
| `datametric360-ios.css` | Dieselbe Optik-Auflage, aber für die `@audi-dash/ui`-Komponentenklassen (`.ads-*`) statt für die Panel-eigenen Klassen - für später, wenn `companion-app/` eine UI bekommt. |
|
||||||
| `support.js` | Hilfsfunktionen des Entwurfs |
|
| `support.js` | Hilfsfunktionen des Entwurfs |
|
||||||
| `assets/` | Icons, Logos, Typenschilder und Schriften, die der Entwurf verwendet |
|
| `assets/` | Icons, Logos, Typenschilder und Schriften, die der Entwurf verwendet |
|
||||||
|
|||||||
@@ -93,8 +93,13 @@ Anders als früher wird dafür **nicht** mehr `pyscript/modules/
|
|||||||
einstellungen.py` von Hand bearbeitet — das übernimmt ein grafisches
|
einstellungen.py` von Hand bearbeitet — das übernimmt ein grafisches
|
||||||
Setup-Menü direkt in der App: **Mein Audi → Einstellungen → Fahrzeug
|
Setup-Menü direkt in der App: **Mein Audi → Einstellungen → Fahrzeug
|
||||||
einrichten → Einrichten → „Setup — Sensoren zuordnen"** (letzter Punkt,
|
einrichten → Einrichten → „Setup — Sensoren zuordnen"** (letzter Punkt,
|
||||||
erscheint erst nach Klick auf „Einrichten"). Dafür muss die App bereits
|
erscheint erst nach Klick auf „Einrichten").
|
||||||
laufen, siehe Schritt 9 zuerst.
|
|
||||||
|
> **Reihenfolge beachten:** Dieser Schritt braucht eine bereits laufende App.
|
||||||
|
> Arbeite deshalb erst die Schritte 5 bis 9 ab (Token, Neustart, Prüfung,
|
||||||
|
> Restarbeiten, Frontend) und komm dann hierher zurück. Schritt 7 verweist
|
||||||
|
> seinerseits auf die hier vorgenommene Zuordnung — das ist kein Widerspruch,
|
||||||
|
> sondern schlicht die Reihenfolge: erst starten, dann zuordnen, dann prüfen.
|
||||||
|
|
||||||
Das Setup-Menü listet jede Sensor-Rolle, die die App kennt (Zündung/
|
Das Setup-Menü listet jede Sensor-Rolle, die die App kennt (Zündung/
|
||||||
Fahrterkennung, Kilometerstand, Tankfüllstand, Standort, Batteriespannung,
|
Fahrterkennung, Kilometerstand, Tankfüllstand, Standort, Batteriespannung,
|
||||||
@@ -104,6 +109,16 @@ anzeigen" grenzt auf die erwartete Domäne/Einheit ein) und schreibt die
|
|||||||
Auswahl direkt in eine Override-Datei — `einstellungen.py` selbst bleibt
|
Auswahl direkt in eine Override-Datei — `einstellungen.py` selbst bleibt
|
||||||
unverändert.
|
unverändert.
|
||||||
|
|
||||||
|
Die Override-Datei ist `/config/audi_dashboard/entitaeten.json`. Sie wird von
|
||||||
|
der automatischen Sicherung und vom Backup-Export mit erfasst; ein
|
||||||
|
Wiederherstellen spielt die Zuordnung also mit zurück.
|
||||||
|
|
||||||
|
**Hinweis zu den Vorgabewerten:** Drei Rollen sind in `einstellungen.py` mit
|
||||||
|
den Test-Entitäten der Entwicklungsinstanz vorbelegt (Zündung, Batterie-
|
||||||
|
spannung, Standort — jeweils `*_testzone_fmm003`). Auf einer frischen
|
||||||
|
Installation zeigen sie ins Leere; das Setup-Menü meldet dann „Entität nicht
|
||||||
|
gefunden". Einfach die eigenen Entitäten zuordnen, damit ist es erledigt.
|
||||||
|
|
||||||
**Zwingend, damit die Fahrterkennung läuft:**
|
**Zwingend, damit die Fahrterkennung läuft:**
|
||||||
- **Zündung/ACC-Status** (`ZUENDUNG_SENSOR`) — ein `binary_sensor`, `on`
|
- **Zündung/ACC-Status** (`ZUENDUNG_SENSOR`) — ein `binary_sensor`, `on`
|
||||||
= Fahrt läuft. Kommt vom Teltonika FMM003 (z. B.
|
= Fahrt läuft. Kommt vom Teltonika FMM003 (z. B.
|
||||||
@@ -226,8 +241,9 @@ Der `panel_custom`-Eintrag aus Schritt 3 zeigt auf
|
|||||||
Prototyp. Das Gerät braucht dafür zusätzlich zur Tailscale-Verbindung
|
Prototyp. Das Gerät braucht dafür zusätzlich zur Tailscale-Verbindung
|
||||||
normalen Internetzugang — sonst bleibt die Karte auf der Fahrt- und
|
normalen Internetzugang — sonst bleibt die Karte auf der Fahrt- und
|
||||||
Tankvorgang-Detailseite leer.
|
Tankvorgang-Detailseite leer.
|
||||||
- **Statistik-Seite** zeigt weiterhin Beispielzahlen aus dem Prototyp,
|
- **Statistik-Seite** wertet die eigenen Fahrten und Tankvorgänge echt aus
|
||||||
keine echte Auswertung der eigenen Fahrten — eigenes Arbeitspaket.
|
(Zeiträume, Verbrauch, Tag/Nacht, privat/Arbeitsweg). Sie bleibt nur so
|
||||||
|
lange leer, wie noch keine Fahrten und Tankungen erfasst sind.
|
||||||
|
|
||||||
## Schritt 10 — künftige Updates einspielen, ohne die App neu zu bauen
|
## Schritt 10 — künftige Updates einspielen, ohne die App neu zu bauen
|
||||||
|
|
||||||
|
|||||||
+16
-6
@@ -96,22 +96,26 @@ Datendateien — nie App-Code oder -Konfiguration.
|
|||||||
`fahrterkennung.py`.
|
`fahrterkennung.py`.
|
||||||
- **Bilder fehlen** (§7a) — Ordner `www/bilder/` ist noch leer. Layout
|
- **Bilder fehlen** (§7a) — Ordner `www/bilder/` ist noch leer. Layout
|
||||||
springt nicht (Maße sind reserviert), aber die Flächen bleiben leer.
|
springt nicht (Maße sind reserviert), aber die Flächen bleiben leer.
|
||||||
- **Statistik-Seite** zeigt weiterhin Beispielzahlen aus dem Prototyp, keine
|
|
||||||
echte Auswertung der erfassten Fahrten.
|
|
||||||
- **Leaflet lädt per CDN**, wie im Prototyp — braucht zusätzlich zu
|
- **Leaflet lädt per CDN**, wie im Prototyp — braucht zusätzlich zu
|
||||||
Tailscale echten Internetzugang auf dem Gerät.
|
Tailscale echten Internetzugang auf dem Gerät.
|
||||||
|
|
||||||
## Was schon funktioniert (getestet)
|
## Was schon funktioniert (getestet)
|
||||||
|
|
||||||
- Fahrterkennung über WLAN mit Pausenregel (`task.unique` + `task.sleep`)
|
- Fahrterkennung über den Zündungssensor mit Pausenregel (`task.unique` + `task.sleep`);
|
||||||
|
bis 2026-08-12 lief sie über den WLAN-Sensor des iPhones
|
||||||
- Zweistufiger Fahrtabschluss per Recorder-Historie-Screening, inklusive
|
- Zweistufiger Fahrtabschluss per Recorder-Historie-Screening, inklusive
|
||||||
Verkettung direkt anschließender Fahrten
|
Verkettung direkt anschließender Fahrten
|
||||||
- Reifenzähler per direkter Subtraktion, kein `utility_meter`
|
- Reifenzähler per direkter Subtraktion, kein `utility_meter`
|
||||||
- Belegverarbeitung inklusive 97-%-Volltankungsregel und Duplikatserkennung
|
- Belegverarbeitung mit Duplikatserkennung über `receipt_key` (die zunächst
|
||||||
über `receipt_key`
|
geplante 97-%-Volltankungsregel wurde per Änderungswunsch entfernt, siehe
|
||||||
|
Kopfkommentar in `pyscript/belegverarbeitung.py`)
|
||||||
- Manuelle Fallback-Wege für Fahrten und Tankvorgänge ohne Beleg
|
- Manuelle Fallback-Wege für Fahrten und Tankvorgänge ohne Beleg
|
||||||
- Frontend: Übersicht, Fahrtenliste, Navigation zwischen allen Ansichten,
|
- Frontend: Übersicht, Fahrtenliste, Navigation zwischen allen Ansichten,
|
||||||
Datenadapter Profil→Oberfläche, Schreibaktionen über `hass.callService`
|
Datenadapter Profil→Oberfläche, Schreibaktionen über `hass.callService`
|
||||||
|
- Statistik-Seite mit echter Auswertung aus Fahrten und Tankvorgängen
|
||||||
|
(Zeiträume, Verbrauch, Tag/Nacht, privat/Arbeitsweg)
|
||||||
|
- Setup-Menü für die Sensor-Zuordnung, inklusive Sicherung in
|
||||||
|
`data/entitaeten.json`
|
||||||
|
|
||||||
## Dateiübersicht
|
## Dateiübersicht
|
||||||
|
|
||||||
@@ -129,12 +133,18 @@ homeassistant/
|
|||||||
│ ├── modules/
|
│ ├── modules/
|
||||||
│ │ ├── einstellungen.py zentrale Entity-ID-Konfiguration (Schritt 4)
|
│ │ ├── einstellungen.py zentrale Entity-ID-Konfiguration (Schritt 4)
|
||||||
│ │ ├── profil.py Datenzugriff (§6)
|
│ │ ├── profil.py Datenzugriff (§6)
|
||||||
|
│ │ ├── entitaeten.py Sensor-Zuordnung aus dem Setup-Menü
|
||||||
│ │ ├── fahrtabschluss_logik.py Screening-Logik (§7.2)
|
│ │ ├── fahrtabschluss_logik.py Screening-Logik (§7.2)
|
||||||
│ │ └── frontend_veroeffentlichung.py Zustände fürs Frontend (§10 Punkt 7 Ersatz)
|
│ │ └── frontend_veroeffentlichung.py Zustände fürs Frontend (§10 Punkt 7 Ersatz)
|
||||||
│ ├── fahrterkennung.py §7.1
|
│ ├── fahrterkennung.py §7.1 (Zündungssensor, früher WLAN)
|
||||||
│ ├── fahrtabschluss.py §7.2 (Trigger-Registrierung)
|
│ ├── fahrtabschluss.py §7.2 (Trigger-Registrierung)
|
||||||
|
│ ├── tankerkennung.py §7.4 (automatische Erkennung am Füllstandsanstieg)
|
||||||
│ ├── reifenzaehler.py §7.5
|
│ ├── reifenzaehler.py §7.5
|
||||||
│ ├── belegverarbeitung.py §7.4, §7.7
|
│ ├── belegverarbeitung.py §7.4, §7.7
|
||||||
|
│ ├── batterieverlauf.py 12-V-Spannung, Tagesminimum/-maximum
|
||||||
|
│ ├── bilderverwaltung.py Upload/Löschen der Fahrzeugfotos
|
||||||
|
│ ├── backup.py tägliche Sicherung und Wiederherstellung
|
||||||
|
│ ├── updateverwaltung.py Update suchen und installieren
|
||||||
│ └── frontend_api.py Lese-/Schreib-Anbindung fürs Frontend
|
│ └── frontend_api.py Lese-/Schreib-Anbindung fürs Frontend
|
||||||
├── www/
|
├── www/
|
||||||
│ ├── audi-dashboard-panel.js Lade-Stub (zeigt configuration.yaml hierher), ändert sich kaum
|
│ ├── audi-dashboard-panel.js Lade-Stub (zeigt configuration.yaml hierher), ändert sich kaum
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_hinweis": "Vorlage ohne echte Fahrzeug-/Personendaten. Vor der Installation nach fahrzeugprofil.json kopieren und die Platzhalter unten ersetzen (VIN, Kennzeichen, Erstzulassung, HU-Termin, WLAN-Name, Versicherung, Werkstatt, Servicebuch) - am einfachsten über den Button 'Einrichten' in den Einstellungen der App, sobald sie einmal läuft. Diese Datei selbst bleibt bewusst im Repo (siehe .gitignore), die echte fahrzeugprofil.json nicht.",
|
"_hinweis": "Vorlage ohne echte Fahrzeug-/Personendaten. Vor der Installation nach fahrzeugprofil.json kopieren und die Platzhalter unten ersetzen (VIN, Kennzeichen, Erstzulassung, HU-Termin, Versicherung, Werkstatt, Servicebuch) - am einfachsten über den Button 'Einrichten' in den Einstellungen der App, sobald sie einmal läuft. Diese Datei selbst bleibt bewusst im Repo (siehe .gitignore), die echte fahrzeugprofil.json nicht.",
|
||||||
|
|
||||||
"fahrzeug": {
|
"fahrzeug": {
|
||||||
"fin": "",
|
"fin": "",
|
||||||
|
|||||||
@@ -21,10 +21,16 @@
|
|||||||
ohne Cache und lädt den eigentlichen Code darüber immer frisch nach
|
ohne Cache und lädt den eigentlichen Code darüber immer frisch nach
|
||||||
(siehe Kopfkommentar in www/audi-dashboard-panel.js).
|
(siehe Kopfkommentar in www/audi-dashboard-panel.js).
|
||||||
|
|
||||||
Absichtlich NICHT kopiert: data/ - das würde ein bereits laufendes
|
Absichtlich NICHT kopiert: die Datenbestände in data/ - das würde ein
|
||||||
Fahrzeugprofil, die Fahrten- und Tankvorgänge-Archive überschreiben. Die
|
bereits laufendes Fahrzeugprofil, die Fahrten- und Tankvorgänge-Archive
|
||||||
data/-Dateien sind nur die Erstbefüllung für die einmalige Installation
|
sowie die Sensor-Zuordnung überschreiben. Diese Dateien sind nur die
|
||||||
(siehe INSTALL.md Schritt 2).
|
Erstbefüllung für die einmalige Installation (siehe INSTALL.md Schritt 2).
|
||||||
|
|
||||||
|
Eine Ausnahme davon ist shell_beleg_parser.py: die Datei liegt zwar in
|
||||||
|
data/, ist aber Code, kein Datenbestand - belegverarbeitung.py ruft sie
|
||||||
|
als Unterprozess aus /config/audi_dashboard/ auf. Ohne diese Ausnahme
|
||||||
|
käme jede Änderung am Belegleser (dem einzigen getesteten Teil des
|
||||||
|
Projekts) auf keiner Instanz an.
|
||||||
|
|
||||||
.PARAMETER Ziel
|
.PARAMETER Ziel
|
||||||
Pfad zum config-Verzeichnis von Home Assistant, z. B. ein per Samba
|
Pfad zum config-Verzeichnis von Home Assistant, z. B. ein per Samba
|
||||||
@@ -55,6 +61,13 @@ Copy-Item "$hier\www\audi-dashboard-panel.js" "$Ziel\www\audi-dashboard-panel.js
|
|||||||
Copy-Item "$hier\www\audi-dashboard.css" "$Ziel\www\audi-dashboard.css" -Force
|
Copy-Item "$hier\www\audi-dashboard.css" "$Ziel\www\audi-dashboard.css" -Force
|
||||||
Copy-Item "$hier\www\audi-dashboard-ios.css" "$Ziel\www\audi-dashboard-ios.css" -Force
|
Copy-Item "$hier\www\audi-dashboard-ios.css" "$Ziel\www\audi-dashboard-ios.css" -Force
|
||||||
|
|
||||||
|
# Code, der in data/ liegt (siehe Kopfkommentar) - der Ordner selbst wird
|
||||||
|
# bewusst nicht gespiegelt, diese eine Datei aber schon.
|
||||||
|
if (Test-Path "$Ziel\audi_dashboard") {
|
||||||
|
Copy-Item "$hier\data\shell_beleg_parser.py" "$Ziel\audi_dashboard\shell_beleg_parser.py" -Force
|
||||||
|
Write-Host " Belegleser aktualisiert"
|
||||||
|
}
|
||||||
|
|
||||||
if (Test-Path "$hier\www\bilder") {
|
if (Test-Path "$hier\www\bilder") {
|
||||||
Write-Host "Kopiere www/bilder/ ..." -ForegroundColor Cyan
|
Write-Host "Kopiere www/bilder/ ..." -ForegroundColor Cyan
|
||||||
New-Item -ItemType Directory -Force -Path "$Ziel\www\bilder" | Out-Null
|
New-Item -ItemType Directory -Force -Path "$Ziel\www\bilder" | Out-Null
|
||||||
|
|||||||
@@ -500,13 +500,6 @@ const STANDORT_PEEK_PX = 96; // sichtbarer Anteil im geschlossenen Zustand, muss
|
|||||||
|
|
||||||
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
|
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
|
||||||
|
|
||||||
/* Fahrzeug-Symbol im myAudi-Stil: einfaches Top-down-Glyph, noch fuer
|
|
||||||
Vorschauen in Gebrauch; die Kartenmarkierung nutzt poi-car (Audi CI). */
|
|
||||||
function fahrzeugGlyphPfade() {
|
|
||||||
return `<path d="M5 15.3 6.4 9.9a2 2 0 0 1 1.9-1.5h7.4a2 2 0 0 1 1.9 1.5l1.4 5.4"/>
|
|
||||||
<path d="M4.6 15.3h14.8a.9.9 0 0 1 .9.9v1.1a.9.9 0 0 1-.9.9H4.6a.9.9 0 0 1-.9-.9v-1.1a.9.9 0 0 1 .9-.9Z"/>
|
|
||||||
<path d="M7.4 11.4h9.2"/>`;
|
|
||||||
}
|
|
||||||
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
|
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
|
||||||
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
|
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
|
||||||
function fahrzeugMarkerSVG(px = 34) {
|
function fahrzeugMarkerSVG(px = 34) {
|
||||||
@@ -514,12 +507,14 @@ function fahrzeugMarkerSVG(px = 34) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
|
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
|
||||||
(siehe fahrterkennung.py), sonst "steht" - erst ab der in "Fahrten
|
(siehe fahrterkennung.py), sonst "steht". Ohne echtes Bewegungssignal vom
|
||||||
zusammenfassen" hinterlegten Pausenzeit (CONFIG.pauseMin) gilt der
|
Fahrzeug (siehe AGENTS.md, FMM003/flespi offen) ist das die beste Näherung
|
||||||
Stillstand als endgültig genug für "Letzter Parkplatz" statt eines
|
aus den bereits vorhandenen Fahrtdaten.
|
||||||
bloßen Zwischenstopps. Ohne echtes Bewegungssignal vom Fahrzeug (siehe
|
|
||||||
AGENTS.md, FMM003/flespi offen) ist das die beste Näherung aus den
|
parkplatz unterscheidet einen endgültigen Stillstand (laenger als die in
|
||||||
bereits vorhandenen Fahrtdaten. */
|
"Fahrten zusammenfassen" hinterlegte Pausenzeit) von einem blossen
|
||||||
|
Zwischenstopp. Das Feld ist vorbereitet, wird aber derzeit von keiner
|
||||||
|
Ansicht gelesen - eine "Letzter Parkplatz"-Anzeige gibt es noch nicht. */
|
||||||
function standortZustand() {
|
function standortZustand() {
|
||||||
const t = TRIPS[0];
|
const t = TRIPS[0];
|
||||||
if (t && t.status === "offen") return { faehrt: true, seitTs: null, parkplatz: false };
|
if (t && t.status === "offen") return { faehrt: true, seitTs: null, parkplatz: false };
|
||||||
@@ -812,7 +807,8 @@ function vStandort() {
|
|||||||
<div class="standort-vollbild">
|
<div class="standort-vollbild">
|
||||||
<div id="smap" class="standort-karte"></div>
|
<div id="smap" class="standort-karte"></div>
|
||||||
<div class="kartensteuerung">
|
<div class="kartensteuerung">
|
||||||
<button type="button" data-standort-layer aria-label="Kartendarstellung wählen">
|
<button type="button" data-standort-layer aria-pressed="${SMAP_STIL === "satellit"}"
|
||||||
|
aria-label="${SMAP_STIL === "satellit" ? "Satellitenansicht, zur Straßenkarte wechseln" : "Straßenkarte, zur Satellitenansicht wechseln"}">
|
||||||
${ciSVG(CI.mapLayer, 22)}
|
${ciSVG(CI.mapLayer, 22)}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-standort-fahrzeug aria-label="Fahrzeug auf der Karte suchen">
|
<button type="button" data-standort-fahrzeug aria-label="Fahrzeug auf der Karte suchen">
|
||||||
@@ -827,7 +823,12 @@ function vStandort() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="standortmenu" id="standortmenu">
|
<div class="standortmenu" id="standortmenu">
|
||||||
<div class="standortmenu-griffzone">
|
<div class="standortmenu-griffzone">
|
||||||
<div class="standortmenu-griff"></div>
|
<!-- Als Knopf, nicht als reines div: sonst laesst sich das Menue
|
||||||
|
ausschliesslich per Wischgeste oder Klick auf den Kartenmarker
|
||||||
|
oeffnen und ist mit Tastatur gar nicht erreichbar. -->
|
||||||
|
<button type="button" class="standortmenu-griff" data-standort-umschalten
|
||||||
|
aria-expanded="${STANDORTMENU_OFFEN}" aria-controls="standortmenu"
|
||||||
|
aria-label="Fahrzeugdetails ${STANDORTMENU_OFFEN ? "einklappen" : "aufklappen"}"></button>
|
||||||
<button type="button" class="standortmenu-schliessen" data-standort-schliessen aria-label="Schließen">
|
<button type="button" class="standortmenu-schliessen" data-standort-schliessen aria-label="Schließen">
|
||||||
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>
|
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -3095,6 +3096,12 @@ function ereignisseVerdrahten() {
|
|||||||
ROOT.getElementById("theme").addEventListener("click", () => {
|
ROOT.getElementById("theme").addEventListener("click", () => {
|
||||||
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
|
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
|
||||||
themeIcon();
|
themeIcon();
|
||||||
|
/* Neu zeichnen, damit auch die Leaflet-Kacheln den Stil wechseln: die
|
||||||
|
Kachel-URL wird beim Aufbau aus TILES[theme] gewaehlt und nicht
|
||||||
|
nachtraeglich aktualisiert. Ohne dies blieb die Karte bis zum naechsten
|
||||||
|
Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht
|
||||||
|
deutlich sichtbar. */
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
|
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
|
||||||
// Logik auch mit der Maus funktioniert (z. B. beim Testen im Browser).
|
// Logik auch mit der Maus funktioniert (z. B. beim Testen im Browser).
|
||||||
@@ -3316,6 +3323,14 @@ function ereignisseVerdrahten() {
|
|||||||
// Klick irgendwo sonst im Popup (aber nicht auf den Catcher, der schon
|
// Klick irgendwo sonst im Popup (aber nicht auf den Catcher, der schon
|
||||||
// oben behandelt wurde) schließt ein offenes Dropdown, ohne das ganze
|
// oben behandelt wurde) schließt ein offenes Dropdown, ohne das ganze
|
||||||
// Popup zu verlassen.
|
// Popup zu verlassen.
|
||||||
|
/* Den Filterschalter VOR dem Schliessen der Auswahlliste behandeln.
|
||||||
|
Sonst ersetzt render() unten das Overlay-DOM, bevor das
|
||||||
|
change-Ereignis des Kontrollkaestchens ausgeliefert wird - der
|
||||||
|
Schalter sprang dann beim naechsten Zeichnen einfach zurueck. */
|
||||||
|
if (e.target.closest("[data-setup-nurpassend]") || e.target.closest(".switch")) {
|
||||||
|
const kasten = ROOT.querySelector("[data-setup-nurpassend]");
|
||||||
|
if (kasten) { setupNurPassend = !setupNurPassend; render(); return; }
|
||||||
|
}
|
||||||
if (setupSucheOffen && !e.target.closest(".entitaet-combo")) { setupSucheOffen = null; setupSuchtext = ""; render(); return; }
|
if (setupSucheOffen && !e.target.closest(".entitaet-combo")) { setupSucheOffen = null; setupSuchtext = ""; render(); return; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3590,6 +3605,7 @@ function ereignisseVerdrahten() {
|
|||||||
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
|
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
|
||||||
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
||||||
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
||||||
|
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
|
||||||
if (e.target.closest("[data-standort-layer]")) { SMAP_STIL = SMAP_STIL === "satellit" ? "strasse" : "satellit"; standortTiles(); return; }
|
if (e.target.closest("[data-standort-layer]")) { SMAP_STIL = SMAP_STIL === "satellit" ? "strasse" : "satellit"; standortTiles(); return; }
|
||||||
if (e.target.closest("[data-standort-fahrzeug]")) { standortAufFahrzeug(); return; }
|
if (e.target.closest("[data-standort-fahrzeug]")) { standortAufFahrzeug(); return; }
|
||||||
if (e.target.closest("[data-standort-user]")) { standortAufUser(); return; }
|
if (e.target.closest("[data-standort-user]")) { standortAufUser(); return; }
|
||||||
|
|||||||
@@ -749,7 +749,19 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
|||||||
}
|
}
|
||||||
.standortmenu.offen { transform: translateY(0); }
|
.standortmenu.offen { transform: translateY(0); }
|
||||||
.standortmenu-griffzone { padding: 10px 0 14px; touch-action: none; cursor: grab; position: relative; }
|
.standortmenu-griffzone { padding: 10px 0 14px; touch-action: none; cursor: grab; position: relative; }
|
||||||
.standortmenu-griff { width: 36px; height: 4px; border-radius: 2px; background: var(--line-strong); margin: 0 auto; }
|
/* Der Griff ist jetzt ein Knopf (Tastaturzugang zum Menue). Die Knopf-
|
||||||
|
Voreinstellungen des Browsers werden deshalb zurueckgesetzt; die
|
||||||
|
Trefferflaeche waechst per Pseudo-Element auf 44px, ohne den sichtbaren
|
||||||
|
Strich zu vergroessern. */
|
||||||
|
.standortmenu-griff {
|
||||||
|
position: relative; display: block; width: 36px; height: 4px;
|
||||||
|
border: 0; padding: 0; border-radius: 2px;
|
||||||
|
background: var(--line-strong); margin: 0 auto; cursor: pointer;
|
||||||
|
}
|
||||||
|
.standortmenu-griff::after {
|
||||||
|
content: ""; position: absolute; left: 50%; top: 50%;
|
||||||
|
transform: translate(-50%, -50%); width: 88px; height: 44px;
|
||||||
|
}
|
||||||
.standortmenu-schliessen {
|
.standortmenu-schliessen {
|
||||||
position: absolute; top: 14px; right: 0; width: 32px; height: 32px; border-radius: 50%;
|
position: absolute; top: 14px; right: 0; width: 32px; height: 32px; border-radius: 50%;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
@@ -762,7 +774,6 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
|||||||
.standortmenu-adresse { font-size: 13.5px; color: var(--fg2); margin-top: 20px; }
|
.standortmenu-adresse { font-size: 13.5px; color: var(--fg2); margin-top: 20px; }
|
||||||
.standortmenu-status { display: flex; align-items: center; gap: 9px; margin-top: 8px; font-size: 13.5px; color: var(--fg2); }
|
.standortmenu-status { display: flex; align-items: center; gap: 9px; margin-top: 8px; font-size: 13.5px; color: var(--fg2); }
|
||||||
.standortmenu-status svg { color: var(--fg3); flex: 0 0 auto; }
|
.standortmenu-status svg { color: var(--fg3); flex: 0 0 auto; }
|
||||||
.dot.neutral { background: var(--fg3); }
|
|
||||||
.standortmenu-werte { display: flex; align-items: center; gap: 10px; margin-top: 8px; font-size: 13.5px; color: var(--fg2); }
|
.standortmenu-werte { display: flex; align-items: center; gap: 10px; margin-top: 8px; font-size: 13.5px; color: var(--fg2); }
|
||||||
.standortmenu-werte svg { color: var(--fg3); flex: 0 0 auto; }
|
.standortmenu-werte svg { color: var(--fg3); flex: 0 0 auto; }
|
||||||
.standortmenu-aktionen { display: flex; gap: 10px; margin-top: 18px; padding-bottom: 4px; }
|
.standortmenu-aktionen { display: flex; gap: 10px; margin-top: 18px; padding-bottom: 4px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user