"""CSV-Import für Fahrten, Tankvorgänge und Wartungsplan (Einstellungen -> "Datensatz laden") - das Gegenstück zum bestehenden, rein clientseitigen CSV-Export (beide Frontends: der "Datensatz sichern"-Popup baut die Datei direkt im Browser aus den schon geladenen Daten, ohne Dienstaufruf). Format identisch zum Export: Semikolon als Trennzeichen, deutsches Zahlenformat ("," als Dezimaltrennzeichen, optional "." als Tausendertrennzeichen), UTF-8 mit BOM (wird beim Lesen übersprungen). ABGLEICH STATT VOLLSTÄNDIGEM ERSATZ (Nutzerentscheidung): eine Zeile mit bekannter ID aktualisiert NUR die in der CSV enthaltenen Spalten am bestehenden Datensatz - alles andere (GPS, Route, edited_fields, Beleg-Verknüpfung, ...) bleibt unangetastet, siehe `belege.tankvorgang_aktualisieren()`s eigene Begründung. Eine Zeile ohne oder mit unbekannter ID legt einen neuen Datensatz an - so lässt sich auch eine von Hand in der Tabelle ergänzte Zeile importieren. Wartungsplan hat, anders als Fahrten/Tankvorgänge, keine eigene ID - die Einträge leben als einfache Liste im Fahrzeugprofil und werden bisher nur über ihren Listenindex adressiert (siehe vSbuch()/Servicebuch() in beiden Frontends). Der Abgleich läuft dort stattdessen über Datum+Art als Ersatzschlüssel - eine Zeile, die eines von beidem ändert, wird deshalb nicht wiedererkannt und legt einen neuen Eintrag an, statt den alten zu aktualisieren. Für die kleinen, von Hand gepflegten Wartungsplan-Listen dieses Projekts ist das ein bewusst akzeptierter Kompromiss gegenüber einer eigens für diesen Zweck eingeführten ID. """ from __future__ import annotations import csv import io import logging from typing import TYPE_CHECKING from . import belege, fahrterkennung if TYPE_CHECKING: from .koordinator import Koordinator _LOGGER = logging.getLogger(__name__) def _de_zahl(text: str | None) -> float | None: """"1.234,5" -> 1234.5. None/leer -> None (Spalte nicht gesetzt, nicht "auf 0 setzen").""" text = (text or "").strip() if not text: return None try: return float(text.replace(".", "").replace(",", ".")) except ValueError: return None def _zeilen(inhalt: str) -> list[dict[str, str]]: """CSV-Text (inkl. Kopfzeile, optional mit BOM) als Liste von Feldname->Text-Wörterbüchern, in der Reihenfolge der Kopfzeile. Ganz leere Zeilen (z. B. eine Leerzeile am Dateiende) werden übersprungen.""" bereinigt = inhalt.lstrip("") zeilen = list(csv.reader(io.StringIO(bereinigt), delimiter=";")) if not zeilen: return [] kopf = zeilen[0] return [dict(zip(kopf, zeile)) for zeile in zeilen[1:] if any(f.strip() for f in zeile)] async def fahrten_importieren(k: Koordinator, inhalt: str) -> dict: bestehende = {f["trip_id"] for f in await k.ablage.fahrten_lesen()} aktualisiert = angelegt = uebersprungen = 0 for zeile in _zeilen(inhalt): trip_id = (zeile.get("ID") or "").strip() start = zeile.get("Start") or None ende = zeile.get("Ende") or None art = zeile.get("Art") or None km = _de_zahl(zeile.get("km")) if trip_id and trip_id in bestehende: await fahrterkennung.aktualisieren(k, { "trip_id": trip_id, "ts_start": start, "ts_end": ende, "art": art, "distanz": km, }) aktualisiert += 1 elif start and ende: await fahrterkennung.manuell_anlegen(k, { "ts_start": start, "ts_end": ende, "art": art, "distanz": km, }) angelegt += 1 else: _LOGGER.warning("CSV-Zeile (Fahrten) ohne Start/Ende übersprungen: %s", zeile) uebersprungen += 1 return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} async def tankvorgaenge_importieren(k: Koordinator, inhalt: str) -> dict: bestehende = {t["tank_id"] for t in await k.ablage.tankvorgaenge_lesen()} aktualisiert = angelegt = uebersprungen = 0 for zeile in _zeilen(inhalt): tank_id = (zeile.get("ID") or "").strip() ts = zeile.get("Zeitpunkt") or None station = zeile.get("Station") or None liter = _de_zahl(zeile.get("Liter")) kosten = _de_zahl(zeile.get("Kosten €")) if tank_id and tank_id in bestehende: daten: dict = {"tank_id": tank_id} if ts is not None: daten["ts"] = ts if liter is not None or kosten is not None: daten["liter"] = liter daten["kosten"] = kosten if station is not None: daten["station"] = station await belege.tankvorgang_aktualisieren(k, daten) aktualisiert += 1 elif ts and liter is not None and kosten is not None: await belege.tankvorgang_manuell(k, { "ts": ts, "liter": liter, "kosten": kosten, "station": station, }) angelegt += 1 else: _LOGGER.warning( "CSV-Zeile (Tankvorgänge) ohne Zeitpunkt/Liter/Kosten übersprungen: %s", zeile ) uebersprungen += 1 return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} async def wartungsplan_importieren(k: Koordinator, inhalt: str) -> dict: profil = await k.ablage.profil_lesen() if profil is None: return {"aktualisiert": 0, "angelegt": 0, "uebersprungen": 0} buch = profil.setdefault("service", {}).setdefault("buch", []) aktualisiert = angelegt = uebersprungen = 0 for zeile in _zeilen(inhalt): datum = zeile.get("Datum") or None art = zeile.get("Art") or None if not datum or not art: _LOGGER.warning("CSV-Zeile (Wartungsplan) ohne Datum/Art übersprungen: %s", zeile) uebersprungen += 1 continue km = _de_zahl(zeile.get("km")) werkstatt = zeile.get("Werkstatt") or None kosten = _de_zahl(zeile.get("Kosten €")) treffer = next( (e for e in buch if e.get("datum") == datum and e.get("art") == art), None ) if treffer is not None: if km is not None: treffer["km"] = km if werkstatt is not None: treffer["werkstatt"] = werkstatt if kosten is not None: treffer["kosten"] = kosten aktualisiert += 1 else: buch.append({ "datum": datum, "km": km or 0, "art": art, "werkstatt": werkstatt or "", "kosten": kosten, "arbeiten": "", }) angelegt += 1 await k.ablage.profil_schreiben(profil) await k.profil_veroeffentlichen() return {"aktualisiert": aktualisiert, "angelegt": angelegt, "uebersprungen": uebersprungen} IMPORTIERER = { "fahrten": fahrten_importieren, "tanken": tankvorgaenge_importieren, "service": wartungsplan_importieren, } async def importieren(k: Koordinator, art: str, inhalt: str) -> dict: funktion = IMPORTIERER.get(art) if funktion is None: _LOGGER.warning("csv_importieren mit unbekannter Art aufgerufen: %s", art) return {"aktualisiert": 0, "angelegt": 0, "uebersprungen": 0} ergebnis = await funktion(k, inhalt) _LOGGER.info("CSV-Import (%s) abgeschlossen - %s", art, ergebnis) return ergebnis