Vollaudit: sechs echte Fehler in Backend, Panel und companion-app behoben

- companion-app Batterie.tsx: Generatorspannung zeigte sich faelschlich als
  Ruhespannung/verzerrte den SOH-Trend, Panel-Filter (AGM_RUHE_MAX_V) fehlte
- Manuell angelegte Fahrten/Tankvorgaenge trugen naive Zeitstempel und wurden
  von der Import-Dublettenpruefung stillschweigend uebersprungen; neue
  gemeinsame zeit_normalisiert() in verlauf.py schliesst die Luecke
- Batteriespannungsverlauf fehlte im Backup (sicherung.py)
- Tankvorgangs-Import verlor stillschweigend aeltere Tankvorgaenge vor Beginn
  der Litersensor-Historie; laeuft jetzt zweigleisig (Prozent + Liter)
- Panel-Statistikseite behauptete faelschlich, der Verbrauch je Fahrt komme
  vom Fahrzeug (OBD) - ist eine Naeherung aus dem Tankfuellstand
- companion-app Statistik.tsx: Arbeitsweg-Segment war noch rot statt neutral

Version 2026.8.27.19, OTA-Buendel neu gebaut, im Testcontainer verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 00:05:58 +02:00
parent 8ba3519090
commit 90fff7b305
12 changed files with 223 additions and 69 deletions
@@ -78,6 +78,29 @@ async def verlauf_lesen(
return []
def zeit_normalisiert(wert: object) -> datetime.datetime | None:
"""Akzeptiert ISO-Zeit mit oder ohne Zeitzone und normalisiert auf UTC.
Ohne Zeitzone gilt die lokale Zeit von Home Assistant - die Oberfläche
schickt sowohl bei manueller Eingabe (Fahrt/Tankvorgang von Hand anlegen
oder bearbeiten, fahrterkennung.py/belege.py) als auch beim rückwirkenden
Import (historienimport.py) Ortszeit, nie UTC. Ohne diese Normalisierung
bliebe ein von Hand erfasster Zeitstempel naiv (kein tzinfo) - und
historienimport.py's eigene Dublettenprüfung (_ueberschneidet() für
Fahrten, bekannte_zeiten für Tankvorgänge) überspringt jeden Datensatz mit
naivem Zeitstempel stillschweigend, statt ihn auf Überschneidung zu
prüfen. Ein von Hand angelegter oder bearbeiteter Datensatz wäre dadurch
nie vor einem doppelten Import geschützt - genau die Lücke, die diese
gemeinsame Funktion schließt, indem jeder Schreibpfad dieselbe
Normalisierung durchläuft."""
if not wert:
return None
ts = datetime.datetime.fromisoformat(str(wert))
if ts.tzinfo is None:
ts = ts.astimezone()
return ts.astimezone(datetime.UTC)
def zahl(wert: object) -> float | None:
try:
return float(wert) # type: ignore[arg-type]