"""Automatische Tankerkennung über den Füllstandssensor. Beobachtung: der Füllstand (in Prozent) springt beim Fahren nie nach oben, er sinkt nur - jeder Anstieg ist also ein Tankvorgang. Schwelle nach Vorgabe: ab +5 Liter ODER +9 Prozentpunkten Anstieg gilt als nachgetankt. Die 5-Liter-Vorgabe wird über fahrzeug.tankvolumen_liter aus dem Fahrzeugprofil in Prozentpunkte umgerechnet, damit beide Angaben auf derselben Einheit verglichen werden können; es gilt jeweils die empfindlichere (kleinere) der beiden Schwellen. Tiefststand-Tracking statt einfachem Vorher/Nachher-Vergleich: 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 Liter/Kosten/Station): 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 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) -> 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 await _automatisch_anlegen(k, anstieg) await k.tiefststand_setzen(aktuell) async def _automatisch_anlegen(k: Koordinator, anstieg_pct: float) -> None: odometer_km = _als_zahl(zustand_oder_none(k.hass, k.zuordnung.werte.KM_SENSOR)) tankvorgang = leerer_tankvorgang( datetime.datetime.now(datetime.UTC).isoformat(), "auto" ) tankvorgang["odometer_km"] = odometer_km tankvorgang["distance_km"] = await k.ablage.distanz_seit_letzter_tankung(odometer_km) await k.ablage.tankvorgang_anhaengen(tankvorgang) await k.tankvorgaenge_veroeffentlichen() _LOGGER.info( "Tankvorgang %s automatisch erkannt (Füllstandsanstieg %s Prozentpunkte)", tankvorgang["tank_id"], round(anstieg_pct, 1), )