TANK_DISTANZ_SENSOR + Datensatz sichern/laden mit echtem CSV-Import (Panel)

- Neuer optionaler Sensor TANK_DISTANZ_SENSOR ersetzt die eigene
  Kilometerstand-Subtraktion fuer die Tankvorgang-Strecke, wo zugeordnet
  (Live-Erkennung, manuelle Erfassung, historischer Import)
- "Fahrzeugprofil" und "Daten ausgeben" zu einer Kachel zusammengelegt:
  "Datensatz sichern" (4 Exporte wie bisher) / "Datensatz laden" (neu:
  echter CSV-Import fuer Fahrten/Tankvorgaenge/Wartungsplan)
- Import gleicht per ID ab (Fahrten/Tankvorgaenge) bzw. Datum+Art
  (Wartungsplan, hat keine eigene ID) und aktualisiert nur die in der CSV
  enthaltenen Spalten - alles andere am Datensatz bleibt unangetastet
- Nebenbei gefunden und behoben: belege.tankvorgang_aktualisieren()
  ueberschrieb bisher immer alle Felder, auch mit None, wenn irgendeins
  geaendert wurde - fuer den CSV-Import gefaehrlich, jetzt nur noch
  tatsaechlich uebergebene Felder
- Ebenfalls gefunden und behoben: Panel las das Import-Ergebnis per
  HASS.states direkt nach dem Dienstaufruf - ein Wettlauf mit dem
  state_changed-Push. Nutzt jetzt denselben HASS.callWS(get_states)-Weg
  wie der bestehende historie_importieren-Ablauf.

companion-app-Portierung von "Datensatz sichern/laden" steht noch aus.

Version 2026.8.28.3, live im Testcontainer verifiziert (alle drei
CSV-Datensatztypen: anlegen + aktualisieren per ID/Datum+Art getestet,
Testdaten danach geloescht).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-28 10:19:06 +02:00
co-authored by Claude Sonnet 5
parent edefdd66d8
commit bfb7dec1e9
11 changed files with 521 additions and 76 deletions
+35 -19
View File
@@ -33,6 +33,7 @@ import os
from typing import TYPE_CHECKING
from . import shell_beleg_parser
from . import tankerkennung
from .tankerkennung import leerer_tankvorgang
from .veroeffentlichung import zustand_oder_none
from .verlauf import zeit_normalisiert
@@ -195,10 +196,10 @@ async def tankvorgang_manuell(k: Koordinator, daten: dict) -> None:
odometer_km = km if km is not None else _km_aus_sensor(k)
distanz = daten.get("distanz")
if distanz is None:
# Vorschlag aus der Differenz zum letzten Tankvorgang, aber wie
# odometer_km selbst frei überschreibbar - ein übergebener Wert hat
# deshalb immer Vorrang vor der Berechnung.
distanz = await k.ablage.distanz_seit_letzter_tankung(odometer_km)
# Vorschlag aus TANK_DISTANZ_SENSOR oder, ohne ihn, der Differenz zum
# letzten Tankvorgang - aber wie odometer_km selbst frei
# überschreibbar, ein übergebener Wert hat deshalb immer Vorrang.
distanz = await tankerkennung.distanz_seit_tankung(k, odometer_km)
ts_roh = daten.get("ts")
ts = zeit_normalisiert(ts_roh) if ts_roh else datetime.datetime.now(datetime.UTC)
@@ -236,26 +237,41 @@ def _km_aus_sensor(k: Koordinator) -> float | None:
async def tankvorgang_aktualisieren(k: Koordinator, daten: dict) -> None:
"""Bearbeitet einen bestehenden Tankvorgang (Einzelbeleg-Seite, egal ob
ursprünglich per Beleg oder manuell angelegt)."""
ursprünglich per Beleg oder manuell angelegt).
Nur tatsächlich in `daten` enthaltene Schlüssel werden geändert - fehlt
ein Feld ganz (statt nur leer/None zu sein), bleibt der bisherige Wert
unangetastet. Das bestehende Bearbeiten-Formular (Einzelbeleg-Seite)
schickt ohnehin immer alle Felder mit, verhält sich also unverändert;
ein schlankerer Aufrufer wie der CSV-Import (csv_import.py) kann so
gezielt nur einzelne Spalten aktualisieren, ohne den Rest des
Datensatzes (odometer_km, discount, fuel_type, receipt_key, ...)
stillschweigend auf None zu setzen."""
tank_id = daten.get("tank_id")
if not tank_id:
_LOGGER.warning("tankvorgang_aktualisieren ohne tank_id aufgerufen")
return
liter = daten.get("liter")
kosten = daten.get("kosten")
ts_normalisiert = zeit_normalisiert(daten.get("ts"))
aenderungen = {
"ts": ts_normalisiert.isoformat() if ts_normalisiert else daten.get("ts"),
"liters": liter,
"fuel_total_eur": kosten,
"price_per_l": preis_je_liter(kosten, liter),
"odometer_km": daten.get("km"),
"discount": daten.get("ersparnis"),
"station_name": daten.get("station"),
"distance_km": daten.get("distanz"),
"fuel_type": daten.get("kraftstoff"),
}
aenderungen: dict = {}
if "ts" in daten:
ts_normalisiert = zeit_normalisiert(daten.get("ts"))
aenderungen["ts"] = ts_normalisiert.isoformat() if ts_normalisiert else daten.get("ts")
if "liter" in daten or "kosten" in daten:
liter = daten.get("liter")
kosten = daten.get("kosten")
aenderungen["liters"] = liter
aenderungen["fuel_total_eur"] = kosten
aenderungen["price_per_l"] = preis_je_liter(kosten, liter)
if "km" in daten:
aenderungen["odometer_km"] = daten.get("km")
if "ersparnis" in daten:
aenderungen["discount"] = daten.get("ersparnis")
if "station" in daten:
aenderungen["station_name"] = daten.get("station")
if "distanz" in daten:
aenderungen["distance_km"] = daten.get("distanz")
if "kraftstoff" in daten:
aenderungen["fuel_type"] = daten.get("kraftstoff")
if daten.get("receipt_key"):
aenderungen["receipt_key"] = daten["receipt_key"]
aenderungen["receipt_file"] = daten.get("receipt_file")