d8b12da36d
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS. Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und "pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die Integration beim ersten Start aus ihrer Vorlage an. Drei alte Schwächen sind dabei mit erledigt: - Die Nutzlast landet nicht mehr in der Recorder-Datenbank (_unrecorded_attributes - das kann nur eine echte Entität). - Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher); fiel sie während eines Ausfalls ins Ende, schließt nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt. - Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg. Namensvertrag geändert, beide Oberflächen mitgezogen: pyscript.audi_dashboard_x -> sensor.audi_dashboard_x, pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das Panel liegt in der Integration und kann nicht driften. Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json. Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport, echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112 Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""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),
|
|
)
|