255a593b50
- Die Update-Kachel hat genau einen Knopf am Ende. Er traegt in jedem Zustand den naechsten faelligen Schritt (suchen, installieren, Neustart, Neuladen); Beschriftung und Fuellung wechseln mit. Die Abschnitte darueber behalten ihre Werte und verlieren nur ihre Knoepfe. - Abstand unter der Trennlinie von 28 auf 16 px: das padding-top verhindert das Kollabieren des margin der Werteliste, also zaehlt jetzt nur noch das padding. - Modell ist in der App ein Auswahlfeld statt eines Textfelds, wie im Panel. Die Liste liegt als Zweitschrift in daten/fahrzeugmodelle.ts; fahrzeugmodelle.test.ts liest die Panel-Quelle ein und vergleicht sie Eintrag fuer Eintrag, damit die beiden nicht auseinanderlaufen. - Faehrt der Wagen, sagen Uebersicht und Sicherheitsseite das, statt "sicher abgestellt" zu behaupten. Die Zuendung meldet der Dongle in Sekunden, Tuer- und Schlossmeldungen kommen verzoegert - die schnellere Quelle hat Vorrang, weil sonst das Wort nicht stimmt. - tankerkennung: der Tiefststand wird vor dem Anlegen gesetzt. Ein gepufferter Schwung Datensaetze legte sonst denselben Tankvorgang zweimal an, weil der zweite Zustandswechsel waehrend des Wartens noch den alten Tiefststand las (04.09.2026, 29,8 l und 29,7 l). - flespi: nach dem Leeren des Zwischenspeichers wird nur abgeglichen, wenn ein Geraetewert vorliegt. Sonst ueberschrieb der Waechterfehler den frisch gelesenen Stand in der Kachel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
8.9 KiB
Python
223 lines
8.9 KiB
Python
"""Automatische Tankerkennung über den Füllstandssensor - oder, wenn
|
|
zugeordnet, direkt über einen Tankvolumen-Sensor in Litern.
|
|
|
|
Beobachtung: der Füllstand springt beim Fahren nie nach oben, er sinkt nur -
|
|
jeder Anstieg ist also ein Tankvorgang. Zwei Datenquellen dafür, in
|
|
absteigender Genauigkeit:
|
|
|
|
- TANK_LITER_SENSOR (optional, z. B. vom CAN): Anstieg direkt in Litern,
|
|
Schwelle LITER_SCHWELLE. Braucht keine Umrechnung und liefert den Anstieg
|
|
selbst gleich als Vorbelegung für die getankte Menge mit.
|
|
- TANK_SENSOR (Prozent, siehe fuellstand_geaendert()): Schwelle nach Vorgabe
|
|
ab +5 Liter ODER +9 Prozentpunkten Anstieg, wobei die 5-Liter-Vorgabe über
|
|
fahrzeug.tankvolumen_liter aus dem Fahrzeugprofil in Prozentpunkte
|
|
umgerechnet wird (beide Angaben so auf derselben Einheit); es gilt jeweils
|
|
die empfindlichere (kleinere) der beiden Schwellen.
|
|
|
|
Ist der Litersensor zugeordnet, übernimmt er die Erkennung vollständig -
|
|
beide gleichzeitig auszuwerten würde denselben Tankvorgang zweimal anlegen.
|
|
Ohne ihn bleibt es beim Prozentsensor wie bisher.
|
|
|
|
Tiefststand-Tracking statt einfachem Vorher/Nachher-Vergleich, für beide
|
|
Quellen gleichermaßen: die Datenquelle liefert einen Tankvorgang oft in
|
|
mehreren kleinen Schritten (40% -> 45% -> 60%), von denen keiner allein die
|
|
Schwelle überschreiten muss. Deshalb wird der zuletzt bekannte Tiefststand
|
|
gespeichert und der Anstieg dagegen gemessen. Nach Anlage eines Tankvorgangs
|
|
wird der Tiefststand auf den aktuellen Wert zurückgesetzt, damit derselbe
|
|
Vorgang nicht mehrfach Datensätze erzeugt.
|
|
|
|
Der so angelegte Tankvorgang ist bewusst ein unvollständiger Platzhalter
|
|
(status "unvollständig", ohne Kosten/Station, Liter nur als grobe Schätzung
|
|
vom Litersensor oder ganz ohne bei reiner Prozent-Erkennung): erwartet wird
|
|
ein automatisch erkannter Tankvorgang mit Zeitstempel, Kilometerstand und
|
|
gefahrener Distanz, den der Nutzer später per Beleg-Nachtrag vervollständigt.
|
|
|
|
Auch hier gilt die Härtung aus fahrterkennung.py: der Tiefststand lebt nicht
|
|
mehr nur im Arbeitsspeicher, sondern im Store des Koordinators und überlebt
|
|
damit einen Neustart.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .ablage import neue_id
|
|
from .veroeffentlichung import zustand_oder_none
|
|
from .verlauf import geraetezeit
|
|
|
|
if TYPE_CHECKING:
|
|
from .koordinator import Koordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
LITER_SCHWELLE = 5
|
|
PROZENT_SCHWELLE = 9
|
|
STANDARD_TANKVOLUMEN_LITER = 58 # Rückfall, falls im Profil nicht hinterlegt
|
|
|
|
|
|
def _als_zahl(wert: object) -> float | None:
|
|
try:
|
|
return float(wert) # type: ignore[arg-type]
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def schwelle_prozent(profil: dict | None) -> float:
|
|
"""Die empfindlichere der beiden Schwellen, in Prozentpunkten."""
|
|
if profil is None:
|
|
tankvolumen = STANDARD_TANKVOLUMEN_LITER
|
|
else:
|
|
tankvolumen = (
|
|
profil.get("fahrzeug", {}).get("tankvolumen_liter") or STANDARD_TANKVOLUMEN_LITER
|
|
)
|
|
return min((LITER_SCHWELLE / tankvolumen) * 100, PROZENT_SCHWELLE)
|
|
|
|
|
|
def leerer_tankvorgang(ts: str, quelle: str) -> dict:
|
|
"""Das vollständige Tankvorgang-Schema, alles Unbekannte auf None."""
|
|
return {
|
|
"tank_id": neue_id("f"),
|
|
"receipt_key": None,
|
|
"ts": ts,
|
|
"liters": None,
|
|
"fuel_total_eur": None,
|
|
"price_per_l": None,
|
|
"discount": None,
|
|
"station_name": None,
|
|
"odometer_km": None,
|
|
"distance_km": None,
|
|
"fuel_type": None,
|
|
"source": quelle,
|
|
"status": "unvollständig",
|
|
"receipt_file": None,
|
|
"edited_fields": [],
|
|
}
|
|
|
|
|
|
async def fuellstand_geaendert(
|
|
k: Koordinator, neu: str | None, ereigniszeit: datetime.datetime | None = None
|
|
) -> None:
|
|
aktuell = _als_zahl(neu)
|
|
if aktuell is None:
|
|
return
|
|
|
|
tiefststand = k.tiefststand_pct
|
|
if tiefststand is None or aktuell <= tiefststand:
|
|
await k.tiefststand_setzen(aktuell)
|
|
return
|
|
|
|
anstieg = aktuell - tiefststand
|
|
if anstieg < schwelle_prozent(await k.ablage.profil_lesen()):
|
|
return
|
|
|
|
# Ist ein direkter Litersensor zugeordnet, übernimmt der die Erkennung
|
|
# (siehe tankvolumen_geaendert) - genauer, weil ohne den Umweg über das im
|
|
# Fahrzeugprofil hinterlegte Tankvolumen. Beide gleichzeitig auswerten
|
|
# würde denselben Tankvorgang zweimal anlegen.
|
|
# Reihenfolge wie bei tankvolumen_geaendert() - erst der Tiefststand, dann
|
|
# das Anlegen, sonst legen zwei dicht aufeinanderfolgende Wechsel denselben
|
|
# Tankvorgang zweimal an.
|
|
await k.tiefststand_setzen(aktuell)
|
|
if not k.zuordnung.werte.TANK_LITER_SENSOR:
|
|
await _automatisch_anlegen(
|
|
k, f"Füllstandsanstieg {round(anstieg, 1)} Prozentpunkte", ereigniszeit=ereigniszeit
|
|
)
|
|
|
|
|
|
async def tankvolumen_geaendert(
|
|
k: Koordinator, neu: str | None, ereigniszeit: datetime.datetime | None = None
|
|
) -> None:
|
|
"""Wie fuellstand_geaendert(), aber direkt in Litern statt in Prozent -
|
|
braucht deshalb keine Umrechnung über das Fahrzeugprofil und liefert mit
|
|
dem gemessenen Anstieg selbst eine sinnvolle Vorbelegung für die getankte
|
|
Menge (siehe leerer_tankvorgang() - "liters" bleibt sonst None, bis der
|
|
Nutzer sie von Hand einträgt oder ein Beleg sie liefert)."""
|
|
aktuell = _als_zahl(neu)
|
|
if aktuell is None:
|
|
return
|
|
|
|
tiefststand = k.tiefststand_liter
|
|
if tiefststand is None or aktuell <= tiefststand:
|
|
await k.tiefststand_liter_setzen(aktuell)
|
|
return
|
|
|
|
anstieg = aktuell - tiefststand
|
|
if anstieg < LITER_SCHWELLE:
|
|
return
|
|
|
|
# Der Tiefststand wird VOR dem Anlegen gesetzt, nicht danach.
|
|
# _automatisch_anlegen() wartet auf Geraetezeit und Ablage; in dieser Zeit
|
|
# kommt der naechste Zustandswechsel dran und liest oben noch den ALTEN
|
|
# Tiefststand - beide legen denselben Tankvorgang an. Genau so entstanden
|
|
# am 04.09.2026 um 21:37:04 zwei Eintraege ueber 29,8 l und 29,7 l, als der
|
|
# Dongle einen gepufferten Schwung Datensaetze auf einmal ablieferte und
|
|
# can_fuel_volume in derselben Sekunde 36,8 und 36,7 meldete.
|
|
# tiefststand_liter_setzen() belegt das Feld als erste Anweisung, also
|
|
# bevor es selbst wartet - danach sieht der zweite Wechsel den neuen Wert.
|
|
await k.tiefststand_liter_setzen(aktuell)
|
|
await _automatisch_anlegen(
|
|
k,
|
|
f"Tankvolumenanstieg {round(anstieg, 1)} l",
|
|
liter_schaetzung=anstieg,
|
|
ereigniszeit=ereigniszeit,
|
|
)
|
|
|
|
|
|
async def distanz_seit_tankung(
|
|
k: Koordinator, odometer_km: float | None, jetzt: bool = True
|
|
) -> float | None:
|
|
"""Bevorzugt TANK_DISTANZ_SENSOR (vom Fahrzeug selbst berechnet), fällt
|
|
ohne ihn auf die eigene Kilometerstand-Subtraktion zurück - dieselbe
|
|
"Sensor bevorzugt, Berechnung als Rückfall"-Präferenz wie bei
|
|
TANK_LITER_SENSOR gegenüber TANK_SENSOR oben.
|
|
|
|
`jetzt=False` fuer einen nachgetragenen Tankvorgang: der Sensor kennt nur
|
|
seinen aktuellen Stand, und der gehoert nicht zu einer Tankung von
|
|
vorgestern. Die Kilometerstand-Subtraktion dagegen vergleicht mit dem
|
|
vorherigen Tankvorgang und ist damit zeitunabhaengig - sie bleibt auch
|
|
dann richtig."""
|
|
if jetzt:
|
|
sensor = _als_zahl(
|
|
zustand_oder_none(k.hass, k.zuordnung.werte.TANK_DISTANZ_SENSOR)
|
|
)
|
|
if sensor is not None:
|
|
return sensor
|
|
return await k.ablage.distanz_seit_letzter_tankung(odometer_km)
|
|
|
|
|
|
async def _automatisch_anlegen(
|
|
k: Koordinator,
|
|
anlass: str,
|
|
liter_schaetzung: float | None = None,
|
|
ereigniszeit: datetime.datetime | None = None,
|
|
) -> None:
|
|
odometer_km = _als_zahl(zustand_oder_none(k.hass, k.zuordnung.werte.KM_SENSOR))
|
|
# Der Zeitstempel des Geräts, nicht die Ankunft in Home Assistant -
|
|
# dieselbe Begründung wie bei Fahrtbeginn und -ende, siehe
|
|
# verlauf.geraetezeit(). Ein Tankvorgang, der aus einem gepufferten
|
|
# Datensatz erkannt wird, trug bisher die Uhrzeit des Auftauchens; bei
|
|
# 43200 s Aufzeichnungstakt im Stand konnten das Stunden sein.
|
|
ts = await geraetezeit(
|
|
k.hass,
|
|
k.zuordnung.werte.MELDEZEIT_SENSOR,
|
|
ereigniszeit,
|
|
datetime.datetime.now(datetime.UTC),
|
|
)
|
|
tankvorgang = leerer_tankvorgang(ts.isoformat(), "auto")
|
|
tankvorgang["odometer_km"] = odometer_km
|
|
tankvorgang["distance_km"] = await distanz_seit_tankung(k, odometer_km)
|
|
if liter_schaetzung is not None:
|
|
# Grobe Vorbelegung, keine feststehende Menge - der Status bleibt
|
|
# "unvollständig" (siehe leerer_tankvorgang), bis Preis/Tankstelle
|
|
# dazukommen. Der Nutzer korrigiert sie bei Bedarf selbst, ein
|
|
# nachgetragener Beleg überschreibt sie ohnehin.
|
|
tankvorgang["liters"] = round(liter_schaetzung, 1)
|
|
|
|
await k.ablage.tankvorgang_anhaengen(tankvorgang)
|
|
await k.tankvorgang_nachbereiten()
|
|
_LOGGER.info(
|
|
"Tankvorgang %s automatisch erkannt (%s)", tankvorgang["tank_id"], anlass
|
|
)
|