Files
tobias 2af5c469d6 Ortsnamen aus dem Backend, Verbrauchsfaktor aus dem Tankbeleg, Radzaehler ueber Fahrten
Ortsauflösung serverseitig (geokodierung.py): der Ort steht jetzt in der Fahrt
statt im Zwischenspeicher jedes Geräts. Beide Oberflächen fragen nichts mehr ab.

Verbrauchskorrektur (verbrauchskorrektur.py): Faktor = Beleg-Liter / Summe der
Einzelfahrten, voll zu voll. Statt einer Schranke am Faktor wird der Nenner
geprüft - decken die erkannten Fahrten 90-110 % der Tacho-Spanne ab? Ausgelöst
beim Schreiben eines Tankvorgangs, nicht erst bei der nächsten Fahrt.

Radzähler: summiert die Fahrten des montierten Satzes statt Tacho-Deltas. Der
alte Weg hatte Fahrzeugwechsel als gefahrene Strecke verbucht (375.962 km bei
einem Tacho von 61.823).

Null heißt unbekannt: ein Kilometerstand von 0 oder ein leeres Feld werden zu
None normalisiert, statt an sechs Lesestellen als "Tachostand null" zu gelten.
Momentanwerte der Sensoren gelten nur für einen Tankvorgang von jetzt.

Oberfläche, beide Codebasen: Wertespalte der Fahrtenliste ausgerichtet, Ort ->
Ort in "Zuletzt", "Räder" statt "Reifen", "Termin vereinbart" entfernt,
Markenlogo 20 % größer, Übersicht-Symbol auf volle Rasterbreite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:28:37 +02:00

482 lines
20 KiB
Python

"""Fahrterkennung über den Zündungs-/ACC-Status (SPECIFICATION.md §7.1).
Start: die Zündungs-Entität wechselt auf "on". Ende: sie wechselt auf "off"
und kommt binnen der Pausenzeit nicht zurück.
Die Pausenregel läuft über eine abbrechbare Aufgabe: jede neue Änderung der
Zündung bricht eine noch wartende Ende-Bestätigung ab. Kommt die Zündung
während der Wartezeit zurück, wird die Fahrt dadurch nicht beendet - genau
die Pausenregel aus §7.1. (In der pyscript-Fassung leisteten task.unique() und
task.sleep() dasselbe.)
WAS HIER GEGENÜBER DER PYSCRIPT-FASSUNG BESSER IST: dort lebte der Beginn
einer laufenden Fahrt nur im Arbeitsspeicher. Ein HA-Neustart mitten in einer
Fahrt oder während der Wartezeit verlor diesen Zwischenstand - die Fahrt wurde
dann gar nicht aufgezeichnet. Das war als bekannte Schwäche vermerkt und ist
hier behoben: der Zwischenstand liegt in einem Store (siehe koordinator.py),
überlebt einen Neustart und wird beim Hochfahren wieder aufgenommen.
"""
from __future__ import annotations
import datetime
import logging
from typing import TYPE_CHECKING
from . import reifen, screening
from .ablage import neue_id
from .veroeffentlichung import zustand_oder_none
from .verlauf import (
MINDESTDAUER_S,
fahrtsignal,
geraetezeit,
verlauf_lesen,
zeit_normalisiert,
)
if TYPE_CHECKING:
from .koordinator import Koordinator
_LOGGER = logging.getLogger(__name__)
# Die frühere Pausenregel (STANDARD_PAUSENZEIT_MIN = 15) ist am 2026-08-31
# entfallen. Der FMM003 wendet sie selbst an - in seiner Konfiguration unter
# "Trip Odometer" steht "Ignition OFF Timeout" auf 900 Sekunden. Eine
# zweite Wartezeit hier oben drauf hätte daraus 30 Minuten gemacht.
#
# Zurückholen, falls die Erkennung je wieder ohne das Gerät auskommen muss:
# eine abbrechbare Wartezeit vor fahrt_beenden() legen (k.warte_ende_ab /
# k.warte_ende_ab_abbrechen im Koordinator sind dafür noch da) und die
# Einstellung fahrten_pausenzeit_min in beiden Oberflächen wieder anbieten.
# Der Nachlauf des Geraets zwischen Zuendung aus und Trip-Ende.
#
# Das Trip-Signal des FMM003 endet nicht mit der Zuendung, sondern erst nach
# seinem "Ignition OFF Timeout" (Trip \ Odometer). Es ist damit als Ausloeser
# ideal - es prellt nicht und bringt die Pausentoleranz mit -, liefert aber ein
# Fahrtende, das systematisch um genau diese Spanne zu spaet liegt.
#
# Der Wert ist die Konfiguration des Geraets, nicht unsere Wahl: er steht dort
# auf 900 s. Aendert er sich, gehoert diese Zahl mitgeaendert - sie laesst sich
# nicht aus den Daten ableiten (siehe _nachlauf_gegenpruefen unten, die genau
# das versucht und meldet, wenn es nicht mehr zusammenpasst).
#
# Bewusst KEINE Einstellung in der Oberflaeche: die Zahl gehoert zum Geraet,
# nicht zum Fahrzeug oder zum Nutzer - dieselbe Linie wie bei UNPLAUSIBLE_KMH
# und MIN_STRECKE_VERBRAUCH_KM in verlauf.py.
NACHLAUF_S = 900
# Ab welcher Abweichung zwischen gerechnetem und beobachtetem Ende gewarnt
# wird. Grosszuegig, weil die Zuendung nur so genau bekannt ist, wie das Geraet
# Datensaetze schreibt - beim Fahren alle 10 s, im Stand deutlich seltener.
NACHLAUF_TOLERANZ_S = 120
def leere_fahrt(
start_ts: datetime.datetime,
ende_ts: datetime.datetime,
quelle: str,
reifensatz: str | None = None,
) -> dict:
"""Das vollständige Fahrt-Schema mit allem, was noch nicht bekannt ist,
auf None. Bewusst alle Felder ausgeschrieben: die Oberfläche darf sich auf
ihre Existenz verlassen, und ein fehlendes Feld wäre schwerer zu finden
als ein leeres."""
return {
"trip_id": neue_id("t"),
"ts_start": start_ts.isoformat(),
"ts_end": ende_ts.isoformat(),
"duration_s": int((ende_ts - start_ts).total_seconds()),
"distance_km": None,
"km_quelle": None,
"odo_start": None,
"odo_end": None,
"avg_speed_kmh": None,
"vmax_kmh": None,
# Die reine Messung, unangetastet - damit die Eichung an den
# Betankungen wiederholbar bleibt (verbrauchskorrektur.py).
"verbrauch_roh_l_100km": None,
"verbrauch_faktor": None,
# Was angezeigt wird: roh mal Faktor, oder roh allein.
"verbrauch_l_100km": None,
"start_lat": None,
"start_lon": None,
"end_lat": None,
"end_lon": None,
"start_address": None,
"end_address": None,
# Nur der Ortsname, fuer die Fahrtenliste. Zwei volle Anschriften
# passen dort nicht in eine Zeile. Er wird beim Aufloesen SEPARAT
# gemerkt, nicht nachtraeglich aus der Anschrift geschnitten -
# Begruendung in geokodierung.anschrift_bauen().
"start_stadt": None,
"end_stadt": None,
"art": "privat",
# Welcher Radsatz montiert war. Der Radzaehler summiert danach - siehe
# reifen.py. None heisst "zaehlt fuer keinen Satz": so bleiben Fahrten
# aus der Zeit vor der Umstellung dem Sockel ueberlassen, statt dort
# ein zweites Mal aufzutauchen.
"reifensatz": reifensatz,
# Wieviel von dieser Fahrt dem Radsatz bereits gutgeschrieben ist.
"reifen_gezaehlt_km": None,
"route": None,
"pausen": [],
"source": quelle,
"status": "offen",
"edited_fields": [],
}
async def _echtes_ende(
k: Koordinator,
start_ts: datetime.datetime,
signal_ende: datetime.datetime,
) -> datetime.datetime:
"""Das Fahrtende ohne den Nachlauf des Geraets.
Das Trip-Signal endet NACHLAUF_S nach der Zuendung; abgezogen ergibt das den
Zeitpunkt, zu dem wirklich abgestellt wurde. Die Rechnung stammt vollstaendig
aus der Logik des Geraets und ueberlebt deshalb auch ein Funkloch: der
Datensatz kommt spaeter, traegt aber seine eigene Zeit (siehe geraetezeit()).
War die ganze Fahrt kuerzer als der Nachlauf, bleibt nichts uebrig - dann war
die Zuendung nur kurz an, ohne dass gefahren wurde. Das Ende wird auf den
Beginn geklemmt, und MINDESTDAUER_S in fahrt_beenden() verwirft den Vorgang
anschliessend. Ohne diese Klemme entstuende aus dreissig Sekunden Zuendung
eine Viertelstunde Fahrt.
NUR wenn die Fahrten wirklich vom Trip-Signal ausgeloest werden. Ist
TRIP_SENSOR nicht zugeordnet, uebernimmt die Zuendung diese Aufgabe
(verlauf.fahrtsignal()) - und die hat keinen Nachlauf. Dort abzuziehen
wuerde jede Fahrt um eine Viertelstunde verkuerzen und jede kuerzere ganz
verschwinden lassen. Der Nachlauf gehoert zum Trip-Element des Geraets,
nicht zu uns; ohne dieses Element gibt es ihn nicht."""
if not k.zuordnung.werte.TRIP_SENSOR:
return signal_ende
gerechnet = signal_ende - datetime.timedelta(seconds=NACHLAUF_S)
if gerechnet <= start_ts:
_LOGGER.info(
"Trip-Signal lief %s s, also nicht laenger als der Nachlauf (%s s) - "
"es wurde nicht gefahren",
int((signal_ende - start_ts).total_seconds()), NACHLAUF_S,
)
return start_ts
await _nachlauf_gegenpruefen(k, start_ts, signal_ende, gerechnet)
return gerechnet
async def _nachlauf_gegenpruefen(
k: Koordinator,
start_ts: datetime.datetime,
signal_ende: datetime.datetime,
gerechnet: datetime.datetime,
) -> None:
"""Vergleicht das gerechnete Ende mit dem beobachteten Zuendungswechsel und
meldet, wenn beide nicht zusammenpassen. Aendert nichts - die Rechnung
bleibt massgeblich.
Warum nur eine Meldung und keine Korrektur: wie genau der Zeitpunkt "Zuendung
aus" bekannt ist, haengt an der I/O-Einstellung des Geraets. Steht der
Operand des Zuendungselements auf "Monitoring", erzeugt ein Wechsel keinen
eigenen Datensatz, sondern faehrt nur im naechsten mit - dann ist der
beobachtete Zeitpunkt zu spaet, oft genau der des Trip-Endes. Auf "On Change"
waere er exakt. Solange das nicht feststeht, waere ein Umschalten auf den
beobachteten Wert eine Verschlechterung; eine Abweichung zu protokollieren
ist es nie."""
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
if not sensor or sensor == fahrtsignal(k.zuordnung.werte):
return
punkte = await verlauf_lesen(k.hass, sensor, start_ts, signal_ende)
aus = [ts for ts, wert in punkte if wert == "off"]
if not aus:
return
abweichung = abs((aus[-1] - gerechnet).total_seconds())
if abweichung > NACHLAUF_TOLERANZ_S:
_LOGGER.warning(
"Fahrtende gerechnet auf %s (Trip-Ende minus %s s), die Zuendung ging "
"laut Aufzeichnung aber um %s aus - %.0f s Abweichung. Entweder stimmt "
"NACHLAUF_S nicht mehr mit dem Geraet ueberein, oder das "
"Zuendungselement schreibt keine eigenen Datensaetze.",
gerechnet.isoformat(), NACHLAUF_S, aus[-1].isoformat(), abweichung,
)
async def _geraetezeit(
k: Koordinator,
ereigniszeit: datetime.datetime | None,
standard: datetime.datetime,
) -> datetime.datetime:
"""Die Meldezeit des Geräts - siehe verlauf.geraetezeit()."""
return await geraetezeit(
k.hass, k.zuordnung.werte.MELDEZEIT_SENSOR, ereigniszeit, standard
)
async def zuendung_geaendert(
k: Koordinator,
neu: str | None,
alt: str | None,
ereigniszeit: datetime.datetime | None = None,
) -> None:
"""Reagiert auf jede Zustandsänderung der Zündungs-Entität.
"unavailable"/"unknown" ist kein Zündungszustand, sondern Funkstille des
Trackers - beim FMM003 z. B. in Tiefgaragen oder Funklöchern, auch mitten
in einer laufenden Fahrt. Würde das wie "aus" gewertet, risse jede solche
Lücke eine einzelne Fahrt in zwei auseinander, sobald sie länger als die
Pausenzeit dauert (der Tracker meldet sich zurück, die Zündung steht laut
letztem echten Wert immer noch auf "on", also sieht es wie eine neue
Fahrt aus). Ohne Signal ist deshalb "keine Aussage", nicht "aus": der
Zustand bleibt unangetastet, bis wieder ein echter Wert kommt."""
if neu not in ("on", "off"):
return
# Kein Vorzustand heißt: die Entität taucht gerade erst auf - beim
# Hochfahren von Home Assistant oder nach einem Neuladen der Integration.
# Das ist kein Wechsel der Zündung, sondern eine Registrierung.
#
# Ohne diese Prüfung las der Beobachter das als "Zündung ging gerade an"
# und legte eine Fahrt an, deren Beginn schlicht der Zeitpunkt des
# Neustarts war. Steht das Trip-Signal ohnehin auf "an", entstand so bei
# JEDEM Neustart eine Phantomfahrt - daher der Eintrag vom 30.08.2026,
# 19:43 bis 08:37 des Folgetags, 12,9 Stunden, 0 km.
#
# Eine wirklich laufende Fahrt geht dabei nicht verloren: deren Beginn
# liegt im Store und wird von nach_neustart_fortsetzen() aufgenommen.
# _kilometerstand_geaendert() im Koordinator prüft aus demselben Grund
# schon immer auf einen Vorzustand.
if alt is None:
return
# Eine noch wartende Ende-Bestätigung aus einer vorherigen Änderung
# abbrechen - das ist der Mechanismus hinter der Pausenregel.
k.warte_ende_ab_abbrechen()
an_jetzt = neu == "on"
# Ob eine Fahrt läuft, entscheidet der eigene Zwischenstand
# (fahrt_start_ts) - nicht der vorherige Rohwert der Entität. Der wäre bei
# einer Funklücke "unavailable" statt "on" gewesen, obwohl die Fahrt die
# ganze Zeit lief.
fahrt_laeuft = k.fahrt_start_ts is not None
jetzt = datetime.datetime.now(datetime.UTC)
if an_jetzt and not fahrt_laeuft:
beginn = await _geraetezeit(k, ereigniszeit, jetzt)
await k.fahrt_start_setzen(beginn)
_LOGGER.info(
"Fahrt gestartet um %s (%s)",
k.fahrt_start_ts,
"Gerätezeit" if beginn != jetzt else "Ankunft in HA",
)
# Die Anzeige "fährt/steht" hängt an derselben Entität - sofort neu
# veröffentlichen, statt bis zum nächsten 20-Sekunden-Takt zu warten.
await k.fahrzeugstatus_veroeffentlichen()
return
if not an_jetzt and fahrt_laeuft:
# Ohne Wartezeit: der FMM003 bringt die Pausenregel selbst mit
# (Trip Odometer -> "Ignition OFF Timeout" = 900 s). Was hier
# ankommt, ist bereits eine beendete Fahrt - noch einmal 15 Minuten
# zu warten haette daraus eine halbe Stunde gemacht und zwei
# aufeinanderfolgende Fahrten verschmolzen, die das Geraet sauber
# getrennt hatte.
signal_ende = await _geraetezeit(k, ereigniszeit, jetzt)
# Ein Ende vor dem Beginn kann es nicht geben. Passiert, wenn die
# Meldezeit aus einem älteren Datensatz stammt als der Fahrtbeginn -
# dann ist die Ankunftszeit die ehrlichere Angabe.
if signal_ende < k.fahrt_start_ts:
_LOGGER.warning(
"Meldezeit %s liegt vor dem Fahrtbeginn %s - nehme die Ankunft",
signal_ende.isoformat(), k.fahrt_start_ts.isoformat(),
)
signal_ende = jetzt
# Erst jetzt den Nachlauf des Geraets abziehen: signal_ende ist der
# Zeitpunkt, zu dem das TRIP-SIGNAL endete, nicht der, zu dem das
# Fahrzeug abgestellt wurde.
ende = await _echtes_ende(k, k.fahrt_start_ts, signal_ende)
await fahrt_beenden(k, k.fahrt_start_ts, ende)
await k.fahrzeugstatus_veroeffentlichen()
async def nach_neustart_fortsetzen(k: Koordinator) -> None:
"""Schließt eine Fahrt ab, die während eines HA-Ausfalls zu Ende ging.
Der Zwischenstand einer laufenden Fahrt überlebt jetzt einen Neustart
(Store im Koordinator). Damit entsteht aber ein neuer Fall, den es vorher
nicht gab: ging die Zündung aus, WÄHREND Home Assistant unten war, hat
niemand die Ende-Bestätigung gesehen - der Beobachter bekommt nach dem
Hochfahren keinen Wechsel mehr, weil der längst passiert ist. Ohne diese
Prüfung bliebe die Fahrt für immer offen, und die nächste echte Fahrt
würde an ihren Beginn angehängt.
Läuft bewusst erst, wenn Home Assistant vollständig gestartet ist: mitten
im Einrichten hat die Datenquelle ihren Zustand oft noch nicht gemeldet,
und ein "noch nicht da" ließe sich nicht von "aus" unterscheiden."""
if k.fahrt_start_ts is None:
return
sensor = fahrtsignal(k.zuordnung.werte)
if zustand_oder_none(k.hass, sensor) == "on":
return # fährt noch - der Beobachter übernimmt wie sonst auch
ende_ts = await _letztes_lebenszeichen(k, sensor, k.fahrt_start_ts)
_LOGGER.info(
"Fahrt seit %s wurde während eines Ausfalls beendet - wird jetzt mit "
"Ende %s abgeschlossen", k.fahrt_start_ts.isoformat(), ende_ts.isoformat(),
)
await fahrt_beenden(k, k.fahrt_start_ts, ende_ts)
async def _letztes_lebenszeichen(
k: Koordinator, sensor: str, seit: datetime.datetime
) -> datetime.datetime:
"""Der zuletzt aufgezeichnete Zeitpunkt der Zündungs-Entität seit `seit`.
Das ist der ehrlichste verfügbare Endzeitpunkt: bis dahin wissen wir, dass
das Fahrzeug beobachtet wurde. "Jetzt" zu nehmen wäre falsch, wenn Home
Assistant tagelang unten war - daraus würde eine Fahrt über Tage. Gibt es
gar keine Aufzeichnung (Sensor nicht zugeordnet, recorder zu kurz), bleibt
nur "jetzt"; das Screening trägt die Kilometerstände ohnehin nach."""
jetzt = datetime.datetime.now(datetime.UTC)
punkte = await verlauf_lesen(k.hass, sensor, seit, jetzt)
return punkte[-1][0] if punkte else jetzt
async def fahrt_beenden(
k: Koordinator, start_ts: datetime.datetime, ende_ts: datetime.datetime
) -> None:
dauer_s = int((ende_ts - start_ts).total_seconds())
if dauer_s < MINDESTDAUER_S:
# Zündung an, aber nicht gefahren - oder ein prellender
# Zündungseingang. Der Import verwirft solche Fenster seit jeher;
# hier taten wir es nicht, und der Bestand füllte sich mit
# Nulleinträgen (siehe MINDESTDAUER_S in verlauf.py). Der laufende
# Start wird trotzdem zurückgesetzt, sonst hinge er für immer.
await k.fahrt_start_setzen(None)
_LOGGER.info(
"Zündung war nur %s s an - keine Fahrt angelegt (Grenze %s s)",
dauer_s,
MINDESTDAUER_S,
)
return
fahrt = leere_fahrt(start_ts, ende_ts, "ha", reifen.aktiver_satz(await k.ablage.profil_lesen()))
await k.ablage.fahrt_anhaengen(fahrt)
await k.fahrt_start_setzen(None)
await k.fahrten_veroeffentlichen()
_LOGGER.info(
"Fahrt %s beendet, Status 'offen', Screening angestoßen", fahrt["trip_id"]
)
await screening.durchfuehren(k)
# ------------------------------------------------------- Aus der Oberfläche
HANDFELDER = {"start_address", "end_address", "odo_start", "odo_end", "distance_km"}
def _handfelder(
start_ort: str | None,
ziel_ort: str | None,
odo_start: float | None,
odo_end: float | None,
distanz: float | None,
) -> dict:
"""Die von Hand befüllbaren Felder auf ihre Datensatz-Namen abgebildet,
ohne die leer gelassenen. Der Aufrufer schreibt die Schlüssel zusätzlich
nach edited_fields - das schützt sie vor dem automatischen
Kilometerstand-Screening, das sonst später darüberschreiben würde."""
werte = {
"start_address": start_ort or None,
"end_address": ziel_ort or None,
"odo_start": odo_start,
"odo_end": odo_end,
"distance_km": distanz,
}
felder = {k: v for k, v in werte.items() if v is not None}
# Fehlt die Distanz, ergibt sie sich aus den beiden Kilometerständen -
# dieselbe Rechnung wie im Screening.
if "distance_km" not in felder and "odo_start" in felder and "odo_end" in felder:
felder["distance_km"] = round(felder["odo_end"] - felder["odo_start"], 1)
return felder
async def manuell_anlegen(k: Koordinator, daten: dict) -> None:
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
automatischen Zündungs-Erkennung. Alles außer Start- und Endzeitpunkt ist
optional und bleibt leer, wenn es das Screening später selbst ergänzen
soll."""
start_ts = zeit_normalisiert(daten["ts_start"])
ende_ts = zeit_normalisiert(daten["ts_end"])
hand = _handfelder(
daten.get("start_ort"), daten.get("ziel_ort"),
daten.get("odo_start"), daten.get("odo_end"), daten.get("distanz"),
)
fahrt = leere_fahrt(
start_ts, ende_ts, "manual", reifen.aktiver_satz(await k.ablage.profil_lesen())
)
fahrt["art"] = daten.get("art") or "privat"
fahrt.update(hand)
fahrt["edited_fields"] = sorted(hand)
if fahrt["distance_km"] is not None:
fahrt["km_quelle"] = "manuell"
fahrt["status"] = "vollständig"
await k.ablage.fahrt_anhaengen(fahrt)
await k.fahrten_veroeffentlichen()
await screening.durchfuehren(k)
_LOGGER.info("Fahrt %s von Hand angelegt", fahrt["trip_id"])
async def aktualisieren(k: Koordinator, daten: dict) -> None:
"""Bearbeitet eine bestehende Fahrt (Einzelfahrt-Seite), egal ob sie
automatisch erkannt oder von Hand angelegt wurde."""
trip_id = daten.get("trip_id")
if not trip_id:
_LOGGER.warning("fahrt_aktualisieren ohne trip_id aufgerufen")
return
aenderungen = _handfelder(
daten.get("start_ort"), daten.get("ziel_ort"),
daten.get("odo_start"), daten.get("odo_end"), daten.get("distanz"),
)
if daten.get("art"):
aenderungen["art"] = daten["art"]
if daten.get("ts_start") and daten.get("ts_end"):
start_ts = zeit_normalisiert(daten["ts_start"])
ende_ts = zeit_normalisiert(daten["ts_end"])
aenderungen["ts_start"] = start_ts.isoformat()
aenderungen["ts_end"] = ende_ts.isoformat()
aenderungen["duration_s"] = int((ende_ts - start_ts).total_seconds())
if aenderungen.get("distance_km") is not None:
aenderungen["km_quelle"] = "manuell"
aenderungen["status"] = "vollständig"
# Von Hand gesetzte Felder bleiben von Hand gesetzt: die Liste wächst mit,
# damit ein späteres Screening sie nicht wieder überschreibt.
passend = [f for f in await k.ablage.fahrten_lesen() if f.get("trip_id") == trip_id]
bisher = set(passend[0].get("edited_fields", [])) if passend else set()
aenderungen["edited_fields"] = sorted(bisher | (HANDFELDER & set(aenderungen)))
if await k.ablage.fahrt_bearbeiten(trip_id, aenderungen):
await k.fahrten_veroeffentlichen()
_LOGGER.info("Fahrt %s aktualisiert", trip_id)
else:
_LOGGER.warning("Fahrt %s nicht gefunden", trip_id)
async def loeschen(k: Koordinator, trip_id: str | None) -> None:
if not trip_id:
_LOGGER.warning("fahrt_loeschen ohne trip_id aufgerufen")
return
if await k.ablage.fahrt_loeschen(trip_id):
await k.fahrten_veroeffentlichen()
_LOGGER.info("Fahrt %s gelöscht", trip_id)
else:
_LOGGER.warning("Fahrt %s nicht gefunden", trip_id)