"""Datenzugriff für Fahrzeugprofil, Fahrten und Tankvorgänge (Bauauftrag §6). Importierbar aus anderen pyscript-Dateien mit `import profil`. Drei getrennte Bestände, wie in §6.1 festgelegt: - Fahrzeugprofil: eine JSON-Datei, alles Fahrzeugspezifische - Fahrten: JSON Lines, eine Zeile je Fahrt - Tankvorgänge: JSON Lines Drei Eigenheiten von pyscript, alle an einer echten Testinstanz beobachtet, nicht nur aus der Dokumentation übernommen: 1. Das eingebaute `open()` existiert in pyscript nicht (NameError) - aus Sicherheitsgründen bewusst nicht freigegeben. Der funktionierende Weg ist `task.executor(io.open, pfad, modus)`: `io.open` ist eine echte externe Funktion aus der Standardbibliothek, kein im pyscript-Ordner selbst definierter Code. 2. task.executor() akzeptiert generell nur solche echten externen Funktionen - eigene, im pyscript-Ordner definierte Funktionen weist es mit "pyscript functions can't be called from task.executor" zurück. Deshalb hier nur `io.open` selbst über den Executor, Lesen/Schreiben/Schließen auf dem zurückgegebenen Datei-Objekt direkt (das ist kein bare-name-Aufruf mehr, sondern ein Methodenaufruf auf einem bereits vorhandenen Objekt). 3. Variablen, die innerhalb eines `with ... as f:`-Blocks zugewiesen werden, waren danach außerhalb nicht mehr auffindbar (NameError), deshalb kein `with` - offen/lesen/schließen nacheinander. """ import io import json import os import uuid BASIS = "/config/audi_dashboard" PROFIL_PFAD = f"{BASIS}/fahrzeugprofil.json" FAHRTEN_PFAD = f"{BASIS}/fahrten.jsonl" TANKVORGAENGE_PFAD = f"{BASIS}/tankvorgaenge.jsonl" BATTERIEVERLAUF_PFAD = f"{BASIS}/batteriespannung.jsonl" BELEGE_ORDNER = f"{BASIS}/belege" # ---------------------------------------------------------------- Ordner --- def ordner_sicherstellen(): os.makedirs(BASIS, exist_ok=True) os.makedirs(BELEGE_ORDNER, exist_ok=True) # ----------------------------------------------------------- Fahrzeugprofil def profil_lesen(): f = task.executor(io.open, PROFIL_PFAD, "r") inhalt = f.read() f.close() return json.loads(inhalt) def profil_schreiben(profil): tmp = PROFIL_PFAD + ".tmp" text = json.dumps(profil, ensure_ascii=False, indent=2) f = task.executor(io.open, tmp, "w") f.write(text) f.close() os.replace(tmp, PROFIL_PFAD) # --------------------------------------------------------- JSON-Lines-Basis def _zeilen_lesen(pfad): if not os.path.exists(pfad): return [] f = task.executor(io.open, pfad, "r") inhalt = f.read() f.close() datensaetze = [] for zeile in inhalt.splitlines(): zeile = zeile.strip() if zeile: datensaetze.append(json.loads(zeile)) return datensaetze def _zeilen_schreiben(pfad, datensaetze): tmp = pfad + ".tmp" zeilen = [json.dumps(d, ensure_ascii=False) for d in datensaetze] text = "\n".join(zeilen) if zeilen: text += "\n" f = task.executor(io.open, tmp, "w") f.write(text) f.close() os.replace(tmp, pfad) def _zeile_anhaengen(pfad, datensatz): f = task.executor(io.open, pfad, "a") f.write(json.dumps(datensatz, ensure_ascii=False) + "\n") f.close() # ------------------------------------------------------------------ Fahrten def fahrten_lesen(): return _zeilen_lesen(FAHRTEN_PFAD) def fahrten_schreiben(fahrten): _zeilen_schreiben(FAHRTEN_PFAD, fahrten) def fahrt_anhaengen(fahrt): _zeile_anhaengen(FAHRTEN_PFAD, fahrt) def offene_fahrten(): """Alle Fahrten mit status == 'offen', ältere zuerst.""" fahrten = fahrten_lesen() offen = [f for f in fahrten if f.get("status") == "offen"] return sorted(offen, key=lambda f: f.get("ts_start", "")) def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben): """Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und schreibt den gesamten Bestand neu. Manuell geänderte Felder (edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben. Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive funktionieren nach demselben Muster.""" geaendert = False for d in zeilen: if d.get(id_feld) == id_wert: geschuetzt = set(d.get("edited_fields", [])) for feld, wert in aenderungen.items(): if feld not in geschuetzt: d[feld] = wert geaendert = True break if geaendert: schreiben(zeilen) return geaendert def fahrt_aktualisieren(trip_id, aenderungen): return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben) def fahrt_loeschen(trip_id): fahrten = fahrten_lesen() uebrig = [f for f in fahrten if f.get("trip_id") != trip_id] if len(uebrig) == len(fahrten): return False fahrten_schreiben(uebrig) return True # ------------------------------------------------------------ Tankvorgänge def tankvorgaenge_lesen(): return _zeilen_lesen(TANKVORGAENGE_PFAD) def tankvorgaenge_schreiben(tankvorgaenge): _zeilen_schreiben(TANKVORGAENGE_PFAD, tankvorgaenge) def tankvorgang_anhaengen(tankvorgang): _zeile_anhaengen(TANKVORGAENGE_PFAD, tankvorgang) def tankvorgang_aktualisieren(tank_id, aenderungen): return _datensatz_aktualisieren(tankvorgaenge_lesen(), "tank_id", tank_id, aenderungen, tankvorgaenge_schreiben) def tankvorgang_loeschen(tank_id): tankvorgaenge = tankvorgaenge_lesen() uebrig = [t for t in tankvorgaenge if t.get("tank_id") != tank_id] if len(uebrig) == len(tankvorgaenge): return False tankvorgaenge_schreiben(uebrig) return True def letzter_tankvorgang(): """Der zeitlich jüngste bereits erfasste Tankvorgang (nach ts), oder None, falls noch keiner existiert. Grundlage für die Gefahrene-Distanz-Berechnung beim Anlegen eines neuen Tankvorgangs (§5.5).""" tankvorgaenge = tankvorgaenge_lesen() if not tankvorgaenge: return None return max(tankvorgaenge, key=lambda t: t.get("ts") or "") def distanz_seit_letzter_tankung(aktueller_km): """Gefahrene Distanz seit dem vorherigen Tankvorgang, als Vorschlag für das gleichnamige Formularfeld (§5.5) - frei überschreibbar, genau wie odometer_km selbst. None, wenn kein Kilometerstand oder kein vorheriger Tankvorgang vorliegt (erster Eintrag überhaupt). Gemeinsame Grundlage für Beleg-Erfassung (belegverarbeitung.py) und automatische Tankerkennung (tankerkennung.py).""" if aktueller_km is None: return None letzter = letzter_tankvorgang() if not letzter or letzter.get("odometer_km") is None: return None return round(aktueller_km - letzter["odometer_km"], 1) def tankvorgang_nach_id(tank_id): for t in tankvorgaenge_lesen(): if t.get("tank_id") == tank_id: return t return None def tankvorgang_nach_receipt_key(receipt_key): """Für §7.7 Regel: derselbe Beleg (receipt_key, minutengenau) erzeugt keinen zweiten Datensatz.""" for t in tankvorgaenge_lesen(): if t.get("receipt_key") == receipt_key: return t return None # ------------------------------------------------------- Batteriespannung def batterieverlauf_lesen(): """Ein Eintrag pro Tag ({datum, min, min_ts, max, max_ts}), älteste zuerst - siehe batterieverlauf.py für die Aufzeichnungslogik. min_ts/ max_ts sind die Zeitstempel (ISO, UTC) der jeweiligen Einzelmessung, für die Datum/Uhrzeit-Anzeige beim Antippen des Diagrammpunkts im Frontend - der Punkt selbst zeigt nur den Minimalwert (siehe dortiger Kommentar, warum der aussagekräftig für die Entladung ist).""" return _zeilen_lesen(BATTERIEVERLAUF_PFAD) def batterieverlauf_tageswert_aktualisieren(datum, ts, spannung): """Trägt eine neue Messung in den Tageseintrag für `datum` ein: legt ihn beim ersten Wert des Tages an, erweitert sonst nur min/max samt dem Zeitstempel der jeweils neuen Extremmessung. Das Fahrzeug meldet die Spannung künftig mehrfach pro Stunde (aktive Fahrt) statt nur einmal täglich - der komplette Bestand wird deshalb bei jeder Messung neu geschrieben (wie bei den übrigen JSON-Lines-Beständen hier), was bei einem Eintrag pro Tag über Jahre hinweg unproblematisch bleibt.""" verlauf = batterieverlauf_lesen() for eintrag in verlauf: if eintrag.get("datum") == datum: if spannung < eintrag["min"]: eintrag["min"] = spannung eintrag["min_ts"] = ts if spannung > eintrag["max"]: eintrag["max"] = spannung eintrag["max_ts"] = ts break else: verlauf.append({"datum": datum, "min": spannung, "min_ts": ts, "max": spannung, "max_ts": ts}) _zeilen_schreiben(BATTERIEVERLAUF_PFAD, verlauf) # --------------------------------------------------------------------- IDs def neue_id(praefix): return f"{praefix}-{uuid.uuid4().hex[:12]}"