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:
@@ -1,6 +1,8 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-09-01** (Phantomfahrten beim Neustart abgestellt, Tankstand auf `wert_bei()`
|
||||
**Last updated: 2026-09-01** (Fahrtbeginn und -ende kommen jetzt vom Gerät (`MELDEZEIT_SENSOR`),
|
||||
Manifest `2026.9.1.3`, siehe Abschnitt AY: dazu die Trennung von
|
||||
Trip- und Zündungsrolle und die Gerätezeit für Tankvorgänge. Davor am selben Tag: Phantomfahrten beim Neustart abgestellt, Tankstand auf `wert_bei()`
|
||||
nachgezogen, zwölf Nullfahrten gelöscht, „Open items" von vier MQTT-Karteileichen befreit;
|
||||
Manifest `2026.9.1.1`, siehe Abschnitt AX. Davor am 2026-08-31: Streckenberechnung: `wert_bei()`
|
||||
statt `naechster_wert()` und
|
||||
@@ -6393,6 +6395,129 @@ am 2026-09-01 die zwölf Fahrten mit `distance_km == 0` gelöscht — über den
|
||||
Das ist eine Abkehr von der Linie in Abschnitt W und AV, wo Nullfahrten bewusst stehen blieben.
|
||||
Sie gilt, weil der Eigentümer sie ausdrücklich getroffen hat — nicht als neue Gewohnheit.
|
||||
|
||||
## AY. Der Dongle ist jetzt führend für Fahrtbeginn und -ende (2026.9.1.2)
|
||||
|
||||
Bis hierher war der Fahrtbeginn `datetime.now()` — die Uhrzeit, zu der Home Assistant den
|
||||
Zustandswechsel *verarbeitet*. Hat das Gerät gepuffert (Funkloch, Tiefgarage, Tiefschlaf), liegt
|
||||
die Wahrheit beliebig weit davor. Am 01.09.2026 gemessen: der Datensatz mit dem Fahrtende trug
|
||||
die Gerätezeit **07:29:04** und kam um **07:35:23** an — sechs Minuten und 19 Sekunden später.
|
||||
|
||||
Neue Sensorrolle **`MELDEZEIT_SENSOR`** (`…_message_timestamp`, Unix-Sekunden). Ist sie
|
||||
zugeordnet, stammen Beginn und Ende der Fahrt daraus statt aus der Ankunftszeit.
|
||||
|
||||
### Die Zeitzonenfrage, gemessen statt vermutet
|
||||
|
||||
Der Zeitstempel ist **UTC**, keine Ortszeit. Der Versatz zur UTC-Uhr von Home Assistant lag bei
|
||||
Sekunden (1,3 s / 2,3 s / 232,8 s nach einer Funklücke), nicht bei zwei Stunden. Eine
|
||||
Zeitzonenumrechnung wäre nicht nur unnötig — sie baute einen Zwei-Stunden-Fehler ein. Die
|
||||
Gerätekonfiguration stützt das: `107 = After Time Sync` (das Gerät speichert erst, wenn seine
|
||||
Uhr steht), NTP-Resync alle drei Stunden.
|
||||
|
||||
### Die Wartezeit, und warum sie da sein muss
|
||||
|
||||
Die flespi-Integration setzt die Entitäten eines Datensatzes **nacheinander**: gemessen stand die
|
||||
Zündung um 07:35:23.633 und die zugehörige Meldezeit 2 ms später um 07:35:23.635. Wer sofort
|
||||
liest, bekommt 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 (siehe die Data-Acquisition-Werte unten).
|
||||
|
||||
`_geraetezeit()` wartet deshalb bis zu `MELDEZEIT_FRIST_S = 2.0` in 50-ms-Schritten darauf,
|
||||
dass `last_updated` der Meldezeit den Zeitpunkt des Zündungswechsels erreicht. Dafür reicht der
|
||||
Koordinator die Ereigniszeit jetzt mit durch (`zuendung_geaendert(..., ereigniszeit)`).
|
||||
|
||||
Zwei Notbremsen, beide live ausgelöst und im Protokoll nachgewiesen:
|
||||
|
||||
- **Zukunft** (mehr als 5 Minuten voraus) → verworfen, Ankunftszeit gilt. Eine falsch gestellte
|
||||
Geräteuhr darf die Auswertung nicht mitreißen.
|
||||
- **Ende vor dem Beginn** → verworfen, Ankunftszeit gilt.
|
||||
|
||||
### Live nachgewiesen, alle vier Wege
|
||||
|
||||
| Fall | Ergebnis |
|
||||
|---|---|
|
||||
| Beginn mit Gerätezeit | Fahrt beginnt `08:04:40`, Ankunft war `08:11:04` — 6:24 früher |
|
||||
| Ende mit Gerätezeit | Fahrt `08:04:40 → 08:09:22`, 282 s; Ankunft war `08:11:24` |
|
||||
| Meldezeit vor dem Beginn | Warnung, Ankunft genommen, danach von `MINDESTDAUER_S` verworfen |
|
||||
| Meldezeit in der Zukunft | Warnung, Ankunft genommen |
|
||||
|
||||
Geprüft über den echten Dienst `entitaeten_schreiben` (Rolle zugeordnet, alle übrigen 35
|
||||
Einträge unangetastet); die dabei entstandene Testfahrt wurde über `fahrt_loeschen` wieder
|
||||
entfernt. Bestand danach unverändert 11 Fahrten, keine mit 0 km, keine offene Fahrt.
|
||||
|
||||
### Was das NICHT löst — die Gerätekonfiguration
|
||||
|
||||
Die `.cfg` vom 01.09.2026 (gzip-gepackte Klartext-Parameterliste, mit `gunzip` lesbar) erklärt
|
||||
den Rest, und er liegt nicht im Code:
|
||||
|
||||
| ID | Bedeutung | Wert | Standard |
|
||||
|---|---|---|---|
|
||||
| 102 / 103 | Sleep settings / Timeout | Deep Sleep / 15 min | Deep Sleep / 1 min |
|
||||
| 10000 | *On Stop* Min Period | **43.200 s (12 h)** | 3.600 s |
|
||||
| 10005 | *On Stop* Send Period | **86.400 s (24 h)** | 120 s |
|
||||
| 10050 / 10055 | *Moving* Min Period / Send Period | 10 s / 60 s | 300 s / — |
|
||||
| 11800 | Trip-Szenario | **1 = Low priority** | 0 |
|
||||
| 11803 / 11804 | Start speed / Ignition off timeout | 3 km/h / 900 s | 5 / 60 |
|
||||
| 11806 | Odometer Calculation source | **1 = OBD** | 0 = GNSS |
|
||||
| 101 | Ignition settings | 8 = Engine RPM | 4 = Power Voltage |
|
||||
|
||||
Zwei Werte bestimmen alles Weitere:
|
||||
|
||||
1. **`11800 = Low priority`.** Laut Teltonika-Wiki macht Low priority „an additional record",
|
||||
High priority dagegen „sends event packet **immediately** to the server using GPRS". Fahrtbeginn
|
||||
und -ende warten also auf das nächste Sendefenster — im Stand bis zu 24 Stunden.
|
||||
2. **`11806 = OBD`.** Der Kilometerzähler des Geräts ist damit eine Kopie des Fahrzeugwerts und
|
||||
kann nie feiner sein als der: **ganze Kilometer**. Mit `GNSS` lieferte er nachweislich
|
||||
Meterauflösung (209177,016 → ,019 → ,022). Gemessen an allen vom CAN gelesenen Streckenwerten
|
||||
dieses Fahrzeugs gibt es **keinen** mit einer Nachkommastelle: Distanz seit Tanken, Distanz seit
|
||||
Fehlerlöschung und Reichweite springen in ganzen Kilometern, die Servicedistanz in
|
||||
Zehnerschritten. `segment_mileage` (die Strecke seit dem letzten Datensatz, `11802 = Between
|
||||
records`) folgt derselben Quelle: mit GNSS Meter, mit OBD ganze Kilometer.
|
||||
|
||||
### Trip und Zündung sind zwei Rollen, nicht eine (2026.9.1.3)
|
||||
|
||||
Bis hierher zeigte `ZUENDUNG_SENSOR` auf das Trip-Signal — im Setup stand dann „Zündung/ACC-Status"
|
||||
über einer Trip-Entität, und die echte Zündung war nirgends mehr zugeordnet. Der Eigentümer hat das
|
||||
getrennt haben wollen, zu Recht: es sind zwei verschiedene Aussagen.
|
||||
|
||||
- **`TRIP_SENSOR`** (neu) — löst Fahrtbeginn und -ende aus. Das gefilterte Signal des Geräts.
|
||||
- **`ZUENDUNG_SENSOR`** — nur noch die Anzeige „fährt/steht" im Fahrzeugstatus und auf der
|
||||
Standortkarte. Am Gerät steht die Zündungsquelle jetzt auf `101 = 8` (Engine RPM); ACC und
|
||||
Bordspannung sind raus, weil unzuverlässig. Der Entitätsname trägt „acc" weiterhin im Text, der
|
||||
Inhalt kommt aber aus der Drehzahl.
|
||||
|
||||
`verlauf.fahrtsignal(werte)` ist die eine Stelle, die entscheidet, was Fahrten auslöst: der
|
||||
Trip-Status, wenn zugeordnet, sonst die Zündung. Damit laufen bestehende Installationen unverändert
|
||||
weiter, und Livepfad (`fahrterkennung.py`, `koordinator.py`) und Rückblick
|
||||
(`historienimport.py`) können nicht auseinanderlaufen — dieselbe Regel wie bei
|
||||
`UNPLAUSIBLE_KMH` und `MINDESTDAUER_S`.
|
||||
|
||||
Der Koordinator beobachtet beide: das Fahrtsignal für die Erkennung, die Zündung nur, um den Status
|
||||
sofort neu zu veröffentlichen. Zeigen beide auf dieselbe Entität, entfällt der zweite Beobachter.
|
||||
|
||||
### Auch der Tankvorgang trägt jetzt die Gerätezeit
|
||||
|
||||
`tankerkennung._automatisch_anlegen()` stempelte mit `datetime.now()` — derselbe Fehler wie bei
|
||||
den Fahrten, nur unbemerkt. `geraetezeit()` liegt deshalb jetzt in `verlauf.py` und wird von
|
||||
beiden benutzt; `fahrterkennung._geraetezeit()` ist nur noch eine Weiterleitung.
|
||||
|
||||
Live nachgewiesen: ein aus einem gepufferten Datensatz erkannter Tankvorgang trägt `08:18:45`,
|
||||
während Home Assistant ihn um `08:38:51` verarbeitet hat — **20 Minuten früher**.
|
||||
|
||||
**Der Livetest hat dabei einen echten Fehler gefangen**, den `py_compile` nicht sehen konnte: der
|
||||
Import von `geraetezeit` in `tankerkennung.py` fehlte, und der `NameError` trat erst beim
|
||||
Anlegen auf. Ein weiterer Beleg für die Regel dieses Projekts — kompiliert ist nicht verifiziert.
|
||||
|
||||
### Was am Gerät noch offen ist
|
||||
|
||||
`11800` steht jetzt auf High priority (Fahrtgrenzen werden sofort gesendet), `11806` wird auf
|
||||
GNSS umgestellt. **Damit ist die Streckenrechnung noch nicht angepasst:** geplant ist, die Strecke
|
||||
aus dem GNSS-Zähler zu nehmen (Meterauflösung) und den CAN-Kilometerstand als Anker zu behalten, an
|
||||
dem sich Drift erkennen lässt. Noch nicht gebaut — erst messen, wenn das Gerät wirklich GNSS liefert,
|
||||
sonst testet man gegen ganze Kilometer.
|
||||
|
||||
`11807` (Odometer Value) steht auf 0. Für die geplante Rechnung ist das gleichgültig, weil sie mit
|
||||
Zuwächsen arbeitet; wer den absoluten Wert lesbar haben will, trägt dort den echten Kilometerstand ein.
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||||
|
||||
@@ -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