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>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Fahrterkennung über die WLAN-Verbindung zum Fahrzeug (§7.1).
|
||||
|
||||
Start: der WLAN-Sensor der Companion App meldet den in den Einstellungen
|
||||
hinterlegten Fahrzeug-WLAN-Namen (Fahrzeugprofil, Feld fahrzeug.wlan_name).
|
||||
Ende: die Verbindung bricht ab und kommt binnen der Pausenzeit nicht zurück.
|
||||
|
||||
Die Pausenregel wird über task.unique() umgesetzt: jede neue Änderung des
|
||||
WLAN-Sensors killt eine noch laufende, wartende Bestätigung des vorherigen
|
||||
Ereignisses. Kommt die Verbindung während der Wartezeit zurück, wird dadurch
|
||||
die wartende Ende-Bestätigung automatisch abgebrochen, ohne dass die Fahrt
|
||||
beendet wird - das entspricht der Pausenregel aus §7.1.
|
||||
|
||||
Die Entity-IDs stehen zentral in pyscript/modules/einstellungen.py - dort vor
|
||||
der Installation einmalig anpassen (siehe INSTALL.md).
|
||||
|
||||
EINSCHRÄNKUNG: der Zustand einer laufenden Fahrt (_fahrt_start_ts) lebt nur
|
||||
im Arbeitsspeicher dieses Skripts. Ein HA-Neustart mitten in einer Fahrt oder
|
||||
während der Wartezeit verliert diesen Zwischenstand - die Fahrt würde dann
|
||||
nicht aufgezeichnet. Das ist ein bekannter Punkt für eine spätere Härtung
|
||||
(z. B. Zwischenstand in einer input_datetime-Helper-Entität sichern), hier
|
||||
bewusst nicht mitgebaut, um die Kernlogik nicht zu verkomplizieren, bevor sie
|
||||
sich im echten Betrieb bewährt hat.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import einstellungen
|
||||
import fahrtabschluss_logik
|
||||
import frontend_veroeffentlichung
|
||||
import profil
|
||||
|
||||
_fahrt_start_ts = None
|
||||
|
||||
|
||||
def _profil_wlan_name():
|
||||
p = profil.profil_lesen()
|
||||
return p.get("fahrzeug", {}).get("wlan_name")
|
||||
|
||||
|
||||
def _pausenzeit_sekunden():
|
||||
p = profil.profil_lesen()
|
||||
return p.get("einstellungen", {}).get("fahrten_pausenzeit_min", 15) * 60
|
||||
|
||||
|
||||
@time_trigger("startup")
|
||||
def beim_start():
|
||||
profil.ordner_sicherstellen()
|
||||
|
||||
|
||||
@state_trigger(f"{einstellungen.WLAN_SENSOR}")
|
||||
def wlan_verbindung_geaendert(value=None, old_value=None):
|
||||
# Killt eine noch laufende Ende-Bestätigung aus einer vorherigen
|
||||
# Änderung - das ist der Mechanismus hinter der Pausenregel.
|
||||
task.unique("audi_dashboard_fahrt_ende_pruefung")
|
||||
|
||||
ziel = _profil_wlan_name()
|
||||
if not ziel:
|
||||
log.warning("audi_dashboard: kein fahrzeug.wlan_name im Fahrzeugprofil hinterlegt")
|
||||
return
|
||||
|
||||
global _fahrt_start_ts
|
||||
verbunden_jetzt = (value == ziel)
|
||||
verbunden_vorher = (old_value == ziel)
|
||||
|
||||
if verbunden_jetzt and not verbunden_vorher:
|
||||
if _fahrt_start_ts is None:
|
||||
_fahrt_start_ts = datetime.datetime.now(datetime.timezone.utc)
|
||||
log.info(f"audi_dashboard: Fahrt gestartet um {_fahrt_start_ts.isoformat()}")
|
||||
return
|
||||
|
||||
if verbunden_vorher and not verbunden_jetzt and _fahrt_start_ts is not None:
|
||||
start_ts = _fahrt_start_ts
|
||||
abbruch_ts = datetime.datetime.now(datetime.timezone.utc)
|
||||
wartezeit_s = _pausenzeit_sekunden()
|
||||
|
||||
# task.unique() oben markiert diese Ausführung als die aktuell
|
||||
# gültige. Kommt die Verbindung vor Ablauf der Wartezeit zurück,
|
||||
# ruft der nächste Trigger erneut task.unique() auf und killt genau
|
||||
# diesen task.sleep() - der Code danach läuft dann nie.
|
||||
task.sleep(wartezeit_s)
|
||||
|
||||
_fahrt_beenden(start_ts, abbruch_ts)
|
||||
|
||||
|
||||
def _fahrt_beenden(start_ts, ende_ts):
|
||||
global _fahrt_start_ts
|
||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||
|
||||
fahrt = {
|
||||
"trip_id": profil.neue_id("t"),
|
||||
"ts_start": start_ts.isoformat(),
|
||||
"ts_end": ende_ts.isoformat(),
|
||||
"duration_s": dauer_s,
|
||||
"distance_km": None,
|
||||
"km_quelle": None,
|
||||
"odo_start": None,
|
||||
"odo_end": None,
|
||||
"avg_speed_kmh": None,
|
||||
"start_lat": None,
|
||||
"start_lon": None,
|
||||
"end_lat": None,
|
||||
"end_lon": None,
|
||||
"start_address": None,
|
||||
"end_address": None,
|
||||
"art": "privat",
|
||||
"route": None,
|
||||
"pausen": [],
|
||||
"source": "ha",
|
||||
"status": "offen",
|
||||
"edited_fields": [],
|
||||
}
|
||||
profil.fahrt_anhaengen(fahrt)
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
log.info(f"audi_dashboard: Fahrt {fahrt['trip_id']} beendet, Status 'offen', Screening angestoßen")
|
||||
|
||||
_fahrt_start_ts = None
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat"):
|
||||
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
|
||||
automatischen WLAN-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
|
||||
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||
fahrt = {
|
||||
"trip_id": profil.neue_id("t"),
|
||||
"ts_start": start_ts.isoformat(),
|
||||
"ts_end": ende_ts.isoformat(),
|
||||
"duration_s": dauer_s,
|
||||
"distance_km": None,
|
||||
"km_quelle": None,
|
||||
"odo_start": None,
|
||||
"odo_end": None,
|
||||
"avg_speed_kmh": None,
|
||||
"start_lat": None,
|
||||
"start_lon": None,
|
||||
"end_lat": None,
|
||||
"end_lon": None,
|
||||
"start_address": None,
|
||||
"end_address": None,
|
||||
"art": art,
|
||||
"route": None,
|
||||
"pausen": [],
|
||||
"source": "manual",
|
||||
"status": "offen",
|
||||
"edited_fields": [],
|
||||
}
|
||||
profil.fahrt_anhaengen(fahrt)
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
||||
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
|
||||
§Löschfunktion). Aufruf als pyscript.audi_dashboard_fahrt_loeschen."""
|
||||
if not trip_id:
|
||||
log.warning("audi_dashboard: fahrt_loeschen ohne trip_id aufgerufen")
|
||||
return
|
||||
if profil.fahrt_loeschen(trip_id):
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
log.info(f"audi_dashboard: Fahrt {trip_id} gelöscht")
|
||||
else:
|
||||
log.warning(f"audi_dashboard: Fahrt {trip_id} nicht gefunden")
|
||||
Reference in New Issue
Block a user