Fahrtzeiten vom Geraet, Trip und Zuendung getrennt (2026.9.1.3)
Enthaelt 2026.9.1.2, das nie einzeln ausgeliefert wurde. FAHRT- UND TANKZEITEN KOMMEN VOM GERAET Fahrtbeginn war datetime.now() - die Uhrzeit, zu der Home Assistant den Wechsel verarbeitet. Hat das Geraet gepuffert (Funkloch, Tiefgarage, Tiefschlaf), liegt die Wahrheit beliebig weit davor. Gemessen: ein Datensatz mit Fahrtende trug die Geraetezeit 07:29:04 und kam um 07:35:23 an. Neue Sensorrolle MELDEZEIT_SENSOR (message_timestamp, Unix-Sekunden). Der Wert ist UTC, keine Ortszeit - nachgemessen lag der Versatz zur UTC-Uhr von HA bei Sekunden, nicht bei zwei Stunden; eine Zeitzonenumrechnung baute einen Zwei-Stunden-Fehler ein. geraetezeit() liegt in verlauf.py, weil Fahrt- und Tankerkennung denselben Zeitstempel bestimmen muessen. tankerkennung._automatisch_anlegen() stempelte bisher ebenfalls mit now(); ein aus einem gepufferten Datensatz erkannter Tankvorgang trug damit die Uhrzeit des Auftauchens. Die flespi-Integration setzt die Entitaeten eines Datensatzes nacheinander - gemessen Zuendung 07:35:23.633, zugehoerige Meldezeit 2 ms spaeter. Wer sofort liest, bekommt den vorigen Datensatz: beim Fahren zehn Sekunden daneben, im Stand Stunden (On Stop Min Period = 43200 s). geraetezeit() wartet deshalb bis zu 2 s auf den passenden Wert. Zwei Notbremsen, beide live ausgeloest: Zeitstempel in der Zukunft und Ende vor dem Beginn. TRIP UND ZUENDUNG SIND ZWEI ROLLEN ZUENDUNG_SENSOR zeigte auf das Trip-Signal; im Setup stand "Zuendung/ACC" ueber einer Trip-Entitaet und die echte Zuendung war nirgends zugeordnet. Neu: TRIP_SENSOR loest Fahrten aus, ZUENDUNG_SENSOR liefert nur noch die Anzeige "faehrt/steht". verlauf.fahrtsignal() ist die eine Stelle, die entscheidet - Trip-Status wenn zugeordnet, sonst Zuendung, damit bestehende Installationen unveraendert weiterlaufen und Livepfad und Rueckblick nicht auseinanderlaufen. VERIFIZIERT in audi_ha_test, jeder Weg einzeln: - Fahrtbeginn 08:30:03 (Geraetezeit) statt 08:38:27 (Ankunft) - Fahrtende 08:04:40 -> 08:09:22 statt 08:11:04 -> 08:11:24 - Tankvorgang 08:18:45 statt 08:38:51 - 20 Minuten frueher - Meldezeit in der Zukunft und Ende vor Beginn: Warnung, Ankunft genommen - Zuendung schalten legt keine Fahrt mehr an, Trip-Signal schon Der Livetest fing dabei einen NameError, den py_compile nicht sehen konnte: der Import von geraetezeit in tankerkennung.py fehlte. Kompiliert ist nicht verifiziert. Alle Testdatensaetze wurden ueber die eigenen Dienste wieder entfernt; Bestand unveraendert 11 Fahrten, 13 Tankvorgaenge, keine offene Fahrt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,34 @@ class Sensorzuordnung:
|
||||
# Kilometerstand der EU-Data-Act-Integration ist der richtige.
|
||||
KM_SENSOR: str = ""
|
||||
|
||||
# Das Trip-Signal des Geräts: es beginnt erst, wenn Zündung UND Bewegung
|
||||
# UND "Start Speed" zusammenkommen, und endet erst nach dem
|
||||
# "Ignition OFF Timeout" des Geräts. Damit bringt es die Pausentoleranz
|
||||
# selbst mit, statt dass wir einen zweiten Zeitgeber bauen.
|
||||
#
|
||||
# Bewusst getrennt von ZUENDUNG_SENSOR: die rohe Zündung prellt (am
|
||||
# 29.08.2026 drei Wechsel in 30 ms) und stand am 30./31.08. dreizehn
|
||||
# Stunden am Stück auf "an", ohne dass gefahren wurde. Als Auslöser für
|
||||
# Fahrten taugt sie nicht, als Anzeige "Zündung an" sehr wohl.
|
||||
#
|
||||
# Leer: dann übernimmt ZUENDUNG_SENSOR die Fahrterkennung wie früher -
|
||||
# bestehende Installationen laufen ohne Zutun weiter.
|
||||
TRIP_SENSOR: str = ""
|
||||
|
||||
# Der Zeitstempel, den das Gerät dem Datensatz selbst mitgegeben hat
|
||||
# (Unix-Sekunden, UTC). Optional, aber der einzige Weg zu einem
|
||||
# ehrlichen Fahrtbeginn: ohne ihn ist der Beginn der Moment, in dem
|
||||
# Home Assistant den Wechsel verarbeitet - und der liegt hinter dem
|
||||
# tatsächlichen, sobald das Gerät gepuffert hat. Am 01.09.2026
|
||||
# gemessen: ein Datensatz mit Fahrtende trug die Gerätezeit 07:29:04
|
||||
# und kam um 07:35:23 an, sechs Minuten später.
|
||||
#
|
||||
# UTC, nicht Ortszeit - ebenfalls nachgemessen, der Versatz zur
|
||||
# UTC-Uhr von Home Assistant lag bei Sekunden, nicht bei zwei Stunden.
|
||||
# Eine Zeitzonenumrechnung wäre also nicht nur unnötig, sie baute
|
||||
# einen Zwei-Stunden-Fehler ein.
|
||||
MELDEZEIT_SENSOR: str = ""
|
||||
|
||||
# Tankfüllstand in Prozent - für die automatische Tankerkennung und die
|
||||
# Anzeige in der Übersicht.
|
||||
TANK_SENSOR: str = ""
|
||||
@@ -157,8 +185,13 @@ SCHLUESSEL: set[str] = set(STANDARDWERTE)
|
||||
# "Nur passende Sensoren anzeigen") - eine fehlende oder leere Liste bedeutet
|
||||
# "keine Einschränkung" bzw. "diese Rolle hat üblicherweise keine Einheit".
|
||||
FELDER: list[dict] = [
|
||||
{"key": "ZUENDUNG_SENSOR", "label": "Zündung/ACC-Status", "gruppe": "fahrterkennung",
|
||||
"hinweis": "on = Fahrt läuft, erkennt Fahrtbeginn/-ende.",
|
||||
{"key": "TRIP_SENSOR", "label": "Trip-Status", "gruppe": "fahrterkennung",
|
||||
"hinweis": "on = Fahrt läuft. Das gefilterte Fahrtsignal des Geräts - erkennt Fahrtbeginn und -ende. Ohne Zuordnung übernimmt die Zündung diese Aufgabe.",
|
||||
"domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
|
||||
"beispiel": "trip_status_true_if_trip_started_false_if_stopped",
|
||||
"stichworte": ["trip", "fahrt", "trip status", "journey"]},
|
||||
{"key": "ZUENDUNG_SENSOR", "label": "Zündung", "gruppe": "fahrterkennung",
|
||||
"hinweis": "on = Zündung an. Für die Anzeige \"fährt/steht\"; ohne zugeordneten Trip-Status erkennt sie zusätzlich Fahrtbeginn und -ende.",
|
||||
"domains": ["binary_sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": True,
|
||||
"beispiel": "engine_ignition_or_acc_status",
|
||||
"stichworte": ["zündung", "ignition", "acc", "motor", "engine"]},
|
||||
@@ -167,6 +200,11 @@ FELDER: list[dict] = [
|
||||
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
||||
"beispiel": "total_vehicle_mileage_read_from_can",
|
||||
"stichworte": ["kilometer", "km", "mileage", "odometer", "tacho", "laufleistung"]},
|
||||
{"key": "MELDEZEIT_SENSOR", "label": "Meldezeit des Geräts", "gruppe": "fahrterkennung",
|
||||
"hinweis": "Zeitstempel des Datensatzes in Unix-Sekunden (UTC). Damit stammen Fahrtbeginn und -ende vom Gerät statt von der Ankunft in Home Assistant.",
|
||||
"domains": ["sensor"], "device_classes": [], "units": ["s", "seconds"], "liste": False, "pflicht": False,
|
||||
"beispiel": "message_timestamp",
|
||||
"stichworte": ["meldezeit", "zeitstempel", "timestamp", "message", "zeit"]},
|
||||
{"key": "TANK_SENSOR", "label": "Tankfüllstand", "gruppe": "fahrterkennung",
|
||||
"hinweis": "Füllstand in Prozent - für die automatische Tankerkennung.",
|
||||
"domains": ["sensor"], "device_classes": [], "units": ["%"], "liste": False, "pflicht": False,
|
||||
|
||||
@@ -26,7 +26,13 @@ from typing import TYPE_CHECKING
|
||||
from . import screening
|
||||
from .ablage import neue_id
|
||||
from .veroeffentlichung import zustand_oder_none
|
||||
from .verlauf import MINDESTDAUER_S, verlauf_lesen, zeit_normalisiert
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
fahrtsignal,
|
||||
geraetezeit,
|
||||
verlauf_lesen,
|
||||
zeit_normalisiert,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .koordinator import Koordinator
|
||||
@@ -77,7 +83,23 @@ def leere_fahrt(start_ts: datetime.datetime, ende_ts: datetime.datetime, quelle:
|
||||
|
||||
|
||||
|
||||
async def zuendung_geaendert(k: Koordinator, neu: str | None, alt: str | None) -> None:
|
||||
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
|
||||
@@ -119,9 +141,16 @@ async def zuendung_geaendert(k: Koordinator, neu: str | None, alt: str | None) -
|
||||
# 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:
|
||||
await k.fahrt_start_setzen(datetime.datetime.now(datetime.UTC))
|
||||
_LOGGER.info("Fahrt gestartet um %s", k.fahrt_start_ts)
|
||||
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()
|
||||
@@ -134,7 +163,17 @@ async def zuendung_geaendert(k: Koordinator, neu: str | None, alt: str | None) -
|
||||
# zu warten haette daraus eine halbe Stunde gemacht und zwei
|
||||
# aufeinanderfolgende Fahrten verschmolzen, die das Geraet sauber
|
||||
# getrennt hatte.
|
||||
await fahrt_beenden(k, k.fahrt_start_ts, datetime.datetime.now(datetime.UTC))
|
||||
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 ende < k.fahrt_start_ts:
|
||||
_LOGGER.warning(
|
||||
"Meldezeit %s liegt vor dem Fahrtbeginn %s - nehme die Ankunft",
|
||||
ende.isoformat(), k.fahrt_start_ts.isoformat(),
|
||||
)
|
||||
ende = jetzt
|
||||
await fahrt_beenden(k, k.fahrt_start_ts, ende)
|
||||
|
||||
await k.fahrzeugstatus_veroeffentlichen()
|
||||
|
||||
@@ -157,7 +196,7 @@ async def nach_neustart_fortsetzen(k: Koordinator) -> None:
|
||||
if k.fahrt_start_ts is None:
|
||||
return
|
||||
|
||||
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
||||
sensor = fahrtsignal(k.zuordnung.werte)
|
||||
if zustand_oder_none(k.hass, sensor) == "on":
|
||||
return # fährt noch - der Beobachter übernimmt wie sonst auch
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ from .tankerkennung import LITER_SCHWELLE, leerer_tankvorgang, schwelle_prozent
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
durchschnitt_kmh,
|
||||
fahrtsignal,
|
||||
hoechstwert_im_fenster,
|
||||
route_aus_verlauf,
|
||||
UNPLAUSIBLE_KMH,
|
||||
@@ -440,7 +441,7 @@ async def importieren(k: Koordinator, start: object, ende: object) -> None:
|
||||
|
||||
werte = k.zuordnung.werte
|
||||
verlaeufe = {
|
||||
"zuendung": await verlauf_lesen(k.hass, werte.ZUENDUNG_SENSOR, von, bis),
|
||||
"zuendung": await verlauf_lesen(k.hass, fahrtsignal(werte), von, bis),
|
||||
"km": await verlauf_lesen(k.hass, werte.KM_SENSOR, von, bis),
|
||||
"tank": await verlauf_lesen(k.hass, werte.TANK_SENSOR, von, bis),
|
||||
"tank_liter": await verlauf_lesen(k.hass, werte.TANK_LITER_SENSOR, von, bis),
|
||||
|
||||
@@ -47,7 +47,16 @@ from homeassistant.helpers.event import (
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from . import batterie, fahrterkennung, identitaet, reifen, screening, sicherung, tankerkennung
|
||||
from . import (
|
||||
batterie,
|
||||
fahrterkennung,
|
||||
identitaet,
|
||||
reifen,
|
||||
screening,
|
||||
sicherung,
|
||||
tankerkennung,
|
||||
verlauf,
|
||||
)
|
||||
from .ablage import Ablage
|
||||
from .const import (
|
||||
BUENDEL_INFO,
|
||||
@@ -207,7 +216,13 @@ class Koordinator:
|
||||
self._beobachter.clear()
|
||||
|
||||
werte = self.zuordnung.werte
|
||||
self._beobachten(werte.ZUENDUNG_SENSOR, self._zuendung_geaendert)
|
||||
# Fahrten löst der Trip-Status aus (oder die Zündung, wenn er fehlt).
|
||||
self._beobachten(verlauf.fahrtsignal(werte), self._fahrtsignal_geaendert)
|
||||
# Die Zündung selbst nur noch für die Anzeige "fährt/steht" - sofort
|
||||
# neu veröffentlichen, statt bis zum nächsten 20-Sekunden-Takt zu
|
||||
# warten. Entfällt, wenn sie ohnehin schon das Fahrtsignal ist.
|
||||
if werte.ZUENDUNG_SENSOR != verlauf.fahrtsignal(werte):
|
||||
self._beobachten(werte.ZUENDUNG_SENSOR, self._zuendung_angezeigt)
|
||||
self._beobachten(werte.KM_SENSOR, self._kilometerstand_geaendert)
|
||||
self._beobachten(werte.TANK_SENSOR, self._tankfuellstand_geaendert)
|
||||
self._beobachten(werte.TANK_LITER_SENSOR, self._tankvolumen_geaendert)
|
||||
@@ -227,9 +242,18 @@ class Koordinator:
|
||||
neu = ereignis.data["new_state"]
|
||||
return (neu.state if neu else None, alt.state if alt else None)
|
||||
|
||||
async def _zuendung_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
async def _zuendung_angezeigt(self, _ereignis: Event[EventStateChangedData]) -> None:
|
||||
await self.fahrzeugstatus_veroeffentlichen()
|
||||
|
||||
async def _fahrtsignal_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
neu, alt = self._zustaende(ereignis)
|
||||
await fahrterkennung.zuendung_geaendert(self, neu, alt)
|
||||
# Der Zeitpunkt, zu dem Home Assistant diesen Wechsel gesehen hat -
|
||||
# nicht "jetzt". Die Fahrterkennung braucht ihn, um die Meldezeit des
|
||||
# Geräts demselben Datensatz zuzuordnen (siehe _geraetezeit dort).
|
||||
zustand = ereignis.data["new_state"]
|
||||
await fahrterkennung.zuendung_geaendert(
|
||||
self, neu, alt, zustand.last_updated if zustand else None
|
||||
)
|
||||
|
||||
async def _kilometerstand_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
neu, alt = self._zustaende(ereignis)
|
||||
@@ -240,11 +264,17 @@ class Koordinator:
|
||||
|
||||
async def _tankfuellstand_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
neu, _alt = self._zustaende(ereignis)
|
||||
await tankerkennung.fuellstand_geaendert(self, neu)
|
||||
zustand = ereignis.data["new_state"]
|
||||
await tankerkennung.fuellstand_geaendert(
|
||||
self, neu, zustand.last_updated if zustand else None
|
||||
)
|
||||
|
||||
async def _tankvolumen_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
neu, _alt = self._zustaende(ereignis)
|
||||
await tankerkennung.tankvolumen_geaendert(self, neu)
|
||||
zustand = ereignis.data["new_state"]
|
||||
await tankerkennung.tankvolumen_geaendert(
|
||||
self, neu, zustand.last_updated if zustand else None
|
||||
)
|
||||
|
||||
# ------------------------------------------------- Laufende Fahrt merken
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.1.1",
|
||||
"version": "2026.9.1.3",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": ["@paul"],
|
||||
|
||||
@@ -45,6 +45,7 @@ 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
|
||||
@@ -95,7 +96,9 @@ def leerer_tankvorgang(ts: str, quelle: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def fuellstand_geaendert(k: Koordinator, neu: str | None) -> None:
|
||||
async def fuellstand_geaendert(
|
||||
k: Koordinator, neu: str | None, ereigniszeit: datetime.datetime | None = None
|
||||
) -> None:
|
||||
aktuell = _als_zahl(neu)
|
||||
if aktuell is None:
|
||||
return
|
||||
@@ -114,11 +117,15 @@ async def fuellstand_geaendert(k: Koordinator, neu: str | None) -> None:
|
||||
# Fahrzeugprofil hinterlegte Tankvolumen. Beide gleichzeitig auswerten
|
||||
# würde denselben Tankvorgang zweimal anlegen.
|
||||
if not k.zuordnung.werte.TANK_LITER_SENSOR:
|
||||
await _automatisch_anlegen(k, f"Füllstandsanstieg {round(anstieg, 1)} Prozentpunkte")
|
||||
await _automatisch_anlegen(
|
||||
k, f"Füllstandsanstieg {round(anstieg, 1)} Prozentpunkte", ereigniszeit=ereigniszeit
|
||||
)
|
||||
await k.tiefststand_setzen(aktuell)
|
||||
|
||||
|
||||
async def tankvolumen_geaendert(k: Koordinator, neu: str | None) -> None:
|
||||
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
|
||||
@@ -138,7 +145,10 @@ async def tankvolumen_geaendert(k: Koordinator, neu: str | None) -> None:
|
||||
return
|
||||
|
||||
await _automatisch_anlegen(
|
||||
k, f"Tankvolumenanstieg {round(anstieg, 1)} l", liter_schaetzung=anstieg
|
||||
k,
|
||||
f"Tankvolumenanstieg {round(anstieg, 1)} l",
|
||||
liter_schaetzung=anstieg,
|
||||
ereigniszeit=ereigniszeit,
|
||||
)
|
||||
await k.tiefststand_liter_setzen(aktuell)
|
||||
|
||||
@@ -155,12 +165,24 @@ async def distanz_seit_tankung(k: Koordinator, odometer_km: float | None) -> flo
|
||||
|
||||
|
||||
async def _automatisch_anlegen(
|
||||
k: Koordinator, anlass: str, liter_schaetzung: float | None = None
|
||||
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))
|
||||
tankvorgang = leerer_tankvorgang(
|
||||
datetime.datetime.now(datetime.UTC).isoformat(), "auto"
|
||||
# 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:
|
||||
|
||||
@@ -29,6 +29,8 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -78,6 +80,19 @@ async def verlauf_lesen(
|
||||
return []
|
||||
|
||||
|
||||
def fahrtsignal(werte: object) -> str:
|
||||
"""Die Entität, die Fahrtbeginn und -ende auslöst.
|
||||
|
||||
Der Trip-Status des Geräts, wenn er zugeordnet ist - sonst die Zündung,
|
||||
wie vor der Trennung der beiden Rollen. Damit laufen bestehende
|
||||
Installationen unverändert weiter, ohne dass jemand etwas zuordnen muss.
|
||||
|
||||
Eine gemeinsame Stelle, weil sonst Livepfad (fahrterkennung.py) und
|
||||
Rückblick (historienimport.py) auseinanderlaufen könnten - dieselbe Regel
|
||||
wie bei UNPLAUSIBLE_KMH und MINDESTDAUER_S."""
|
||||
return getattr(werte, "TRIP_SENSOR", "") or getattr(werte, "ZUENDUNG_SENSOR", "")
|
||||
|
||||
|
||||
def zeit_normalisiert(wert: object) -> datetime.datetime | None:
|
||||
"""Akzeptiert ISO-Zeit mit oder ohne Zeitzone und normalisiert auf UTC.
|
||||
|
||||
@@ -139,6 +154,75 @@ def durchschnitt_kmh(distanz_km: float | None, dauer_s: float | None) -> float |
|
||||
return round(distanz_km / (dauer_s / 3600.0), 1)
|
||||
|
||||
|
||||
# Wie lange auf die Meldezeit desselben Datensatzes gewartet wird.
|
||||
#
|
||||
# Die flespi-Integration setzt die Entitäten eines Datensatzes nacheinander:
|
||||
# am 01.09.2026 gemessen stand die Zündung um 07:35:23.633 und die zugehörige
|
||||
# Meldezeit 2 ms später um 07:35:23.635. Ohne diese kurze Wartezeit läse man
|
||||
# den Zeitstempel des VORIGEN Datensatzes - beim Fahren zehn Sekunden daneben,
|
||||
# im Stand aber möglicherweise Stunden, weil das Gerät dort nur alle paar
|
||||
# Stunden einen Satz schreibt (Data Acquisition, On Stop: 43200 s).
|
||||
MELDEZEIT_FRIST_S = 2.0
|
||||
MELDEZEIT_TAKT_S = 0.05
|
||||
|
||||
# Wie weit die Geräteuhr der von Home Assistant vorauslaufen darf, bevor der
|
||||
# Zeitstempel verworfen wird. Etwas Luft für Drift, aber keine Stunden - eine
|
||||
# falsch gestellte Geräteuhr darf die Auswertung nicht mitreißen.
|
||||
MELDEZEIT_VORLAUF = datetime.timedelta(minutes=5)
|
||||
|
||||
|
||||
async def geraetezeit(
|
||||
hass: HomeAssistant,
|
||||
sensor: str | None,
|
||||
ereigniszeit: datetime.datetime | None,
|
||||
standard: datetime.datetime,
|
||||
) -> datetime.datetime:
|
||||
"""Der Zeitstempel, den das Gerät seinem Datensatz mitgegeben hat - oder
|
||||
`standard`, wenn er fehlt oder unglaubwürdig ist.
|
||||
|
||||
Warum das nötig ist: `standard` ist die Uhrzeit, zu der Home Assistant den
|
||||
Zustandswechsel verarbeitet hat. Hat das Gerät gepuffert - Funkloch,
|
||||
Tiefgarage, Tiefschlaf -, liegt die Wahrheit beliebig weit davor. Am
|
||||
01.09.2026 gemessen: ein Datensatz mit Fahrtende trug die Gerätezeit
|
||||
07:29:04 und kam um 07:35:23 an, sechs Minuten später.
|
||||
|
||||
Der Wert ist eine Unix-Sekunde in UTC, nicht Ortszeit - ebenfalls
|
||||
nachgemessen: der Versatz zur UTC-Uhr von Home Assistant lag bei Sekunden,
|
||||
nicht bei zwei Stunden. Eine Zeitzonenumrechnung wäre nicht nur unnötig,
|
||||
sie baute einen Zwei-Stunden-Fehler ein.
|
||||
|
||||
Gemeinsam für Fahrterkennung und Tankerkennung: beide legen Datensätze mit
|
||||
einem Zeitstempel an und dürfen ihn nie unterschiedlich bestimmen - dieselbe
|
||||
Regel wie bei UNPLAUSIBLE_KMH und MINDESTDAUER_S."""
|
||||
if not sensor:
|
||||
return standard
|
||||
|
||||
if ereigniszeit is not None:
|
||||
wartezeit = 0.0
|
||||
while wartezeit < MELDEZEIT_FRIST_S:
|
||||
zustand = hass.states.get(sensor)
|
||||
if zustand is not None and zustand.last_updated >= ereigniszeit:
|
||||
break
|
||||
await asyncio.sleep(MELDEZEIT_TAKT_S)
|
||||
wartezeit += MELDEZEIT_TAKT_S
|
||||
|
||||
zustand = hass.states.get(sensor)
|
||||
try:
|
||||
gemeldet = datetime.datetime.fromtimestamp(
|
||||
float(zustand.state), datetime.UTC # type: ignore[union-attr]
|
||||
)
|
||||
except (AttributeError, TypeError, ValueError, OSError, OverflowError):
|
||||
return standard
|
||||
|
||||
if gemeldet > standard + MELDEZEIT_VORLAUF:
|
||||
_LOGGER.warning(
|
||||
"Meldezeit %s liegt in der Zukunft (jetzt %s) - verworfen",
|
||||
gemeldet.isoformat(), standard.isoformat(),
|
||||
)
|
||||
return standard
|
||||
return gemeldet
|
||||
|
||||
|
||||
def wert_bei(
|
||||
verlauf: list[Verlaufspunkt], zeitpunkt: datetime.datetime
|
||||
) -> float | None:
|
||||
|
||||
Reference in New Issue
Block a user