GNSS-Rollen vorbereitet, "Geparkt seit" von der Zuendung (2026.9.1.5/.6)
GNSS-STRECKENROLLEN, NOCH OHNE VERBRAUCHER 11806 steht seit dem 01.09. auf GNSS. Zwei neue Sensorrollen angelegt, damit nach der ersten echten Fahrt nur noch gemessen und nicht mehr zugeordnet werden muss: GNSS_KM_SENSOR (total_calculated_mileage, der robustere fuer eine Fahrtstrecke - ein verlorener Datensatz verfaelscht die Differenz nicht) und GNSS_TEILSTRECKE_SENSOR (segment_mileage, zeigt WO Strecke fehlt, taugt als Gegenprobe). Kein Code liest sie; die Zuordnung aendert nichts. KM_SENSOR bleibt der Anker - er ist der Tacho des Fahrzeugs und driftet nicht. Der alte Warnhinweis dort zeigt jetzt auf die neue Rolle statt ins Leere. "GEPARKT SEIT" KOMMT VON DER ZUENDUNG Die Anzeige rechnete "jetzt minus ts_end der juengsten Fahrt". Das ist keine Aussage ueber das Parken, sondern ueber die Fahrterkennung: am 01.09. stand dort "Geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden - die Fahrt fehlte im Bestand, weil trip_status festhing. Der Koordinator merkt sich jetzt den Zeitpunkt, zu dem die Zuendung zuletzt ausging, in der Zeit des Geraets. Er liegt im Laufzeit-Store neben fahrt_start_ts und ueberlebt einen Neustart. Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt und wird auch so angezeigt - ausdrueckliche Ansage des Eigentuemers, eine falsche Zahl ist schlechter als keine. Beim Ausliefern fiel derselbe Neustart-Fehler auf wie bei der Phantomfahrt: der Zuhoerer las die Registrierung der Entitaet als Wechsel und stempelte den Parkbeginn bei jedem Neustart neu. if alt is None: return behebt es. Merkposten fuer dieses Projekt: jeder neue Zustandszuhoerer braucht diese Pruefung. VERIFIZIERT in audi_ha_test: Neustart laesst 09:34:15 unveraendert; Zuendung an -> null; Zuendung aus mit Geraetezeit von vor 40 Minuten -> 09:04:57 statt jetzt. Companion-App gleichgezogen, tsc --noEmit sauber, 165/165 Tests gruen. NOTIERT, NICHT GEBAUT: der GNSS-Zaehler driftet im Stand. Die 0,034 km waren GPS-Rauschen, nicht Aufloesung - der Dongle lag unbewegt. Die GNSS-Zuwaechse brauchen deshalb ein Bewegungstor, bevor sie summiert werden duerfen. 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** (Fahrtende ohne den 900-s-Nachlauf des Geräts, Manifest `2026.9.1.4`,
|
||||
**Last updated: 2026-09-01** („Geparkt seit" kommt von der Zündung, Manifest `2026.9.1.6`,
|
||||
Abschnitt BB. Davor: GNSS-Streckenrollen vorbereitet, `2026.9.1.5`, Abschnitt BA.
|
||||
Davor: Fahrtende ohne den 900-s-Nachlauf des Geräts, Manifest `2026.9.1.4`,
|
||||
siehe Abschnitt AZ. Davor am selben Tag: Fahrtbeginn und -ende kommen 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()`
|
||||
@@ -6567,6 +6569,87 @@ Verifiziert in `audi_ha_test`:
|
||||
|
||||
Testfahrt anschließend über `fahrt_loeschen` entfernt; Bestand unverändert 11 Fahrten.
|
||||
|
||||
## BA. Zwei Rollen für die GNSS-Strecke, vorbereitet ohne Verbraucher (2026.9.1.5)
|
||||
|
||||
`11806` steht seit dem 01.09.2026 auf **GNSS**, und der Zähler liefert sofort, was er soll:
|
||||
`total_calculated_mileage = 0.034` kurz nach dem Zurücksetzen — **Meterauflösung**, gegenüber
|
||||
147 Schritten von exakt 1,0 km beim CAN-Wert über die gesamte Aufzeichnung.
|
||||
|
||||
Zwei neue Sensorrollen, **beide noch ohne Verbraucher**:
|
||||
|
||||
- `GNSS_KM_SENSOR` → `total_calculated_mileage`. Der robustere der beiden für eine
|
||||
Fahrtstrecke: ein verlorener Datensatz verfälscht die Differenz nicht, der Zähler läuft weiter.
|
||||
- `GNSS_TEILSTRECKE_SENSOR` → `segment_mileage` (Strecke seit dem letzten Datensatz,
|
||||
`11802 = Between records`). Taugt für eine Summe nur, wenn kein Satz fehlt — zeigt dafür, **wo**
|
||||
Strecke verloren ging, und ist damit die bessere Gegenprobe.
|
||||
|
||||
**Warum leer und ohne Wirkung:** die Rechnung soll gegen echte GNSS-Daten entstehen, nicht gegen
|
||||
erfundene Reihen. Die Rollen sind angelegt und im Testcontainer bereits zugeordnet, damit nach der
|
||||
ersten echten Fahrt sofort gemessen werden kann. Solange kein Code sie liest, ändert die Zuordnung
|
||||
nichts.
|
||||
|
||||
**Die Absicht dahinter, damit sie nicht verlorengeht:** `KM_SENSOR` bleibt der Anker — er ist der
|
||||
Tacho des Fahrzeugs und driftet nicht. Der GNSS-Zähler liefert die Nachkommastelle dazwischen und
|
||||
verliert Strecke, wo kein Empfang ist. Beides zusammen ergibt eine Strecke auf 100 m genau, die
|
||||
trotzdem am Fahrzeugwert hängt. Der alte Warnhinweis bei `KM_SENSOR` („niemals
|
||||
`total_calculated_mileage` zuordnen") gilt unverändert — er zeigt jetzt auf die neue Rolle.
|
||||
|
||||
**Vor dem Zuordnen prüfen:** steht `11802` auf `Continuous` statt `Between records`, zählt
|
||||
`segment_mileage` die laufende Fahrt statt der Teilstrecke und die zweite Rolle ist unbrauchbar.
|
||||
|
||||
Geprüft: 27 FELDER-Einträge, 27 Dataclass-Felder, keine Abweichung in beide Richtungen; Zuordnung
|
||||
über den echten Dienst geschrieben, alle 27 Einträge stehen.
|
||||
|
||||
## BB. „Geparkt seit" kommt von der Zündung, nicht aus der Fahrtenliste (2026.9.1.6)
|
||||
|
||||
Die Anzeige rechnete `jetzt − ts_end` der jüngsten Fahrt. Das ist keine Aussage über das Parken,
|
||||
sondern über die **Fahrterkennung** — und die kann daneben liegen: am 01.09.2026 stand dort
|
||||
„Geparkt seit 2 Tg. 16 Std.", während am Vorabend nachweislich 7 km gefahren wurden. Die Fahrt
|
||||
fehlte im Bestand, weil `trip_status` seit 18:39 festhing.
|
||||
|
||||
Neu: der Koordinator merkt sich den Zeitpunkt, zu dem die Zündung zuletzt auf „aus" ging — **in
|
||||
der Zeit des Geräts** (`verlauf.geraetezeit()`), nicht in der Ankunftszeit. Er liegt im
|
||||
Laufzeit-Store neben `fahrt_start_ts` und überlebt einen Neustart; `veroeffentlichung` gibt ihn
|
||||
als `geparkt_seit` heraus.
|
||||
|
||||
**Kein Rückfall auf die Fahrtenliste.** Ist der Wert `null`, zeigen beide Oberflächen „Fahrzeug
|
||||
steht, Parkdauer unbekannt". Ausdrückliche Ansage des Eigentümers: eine falsche Zahl ist schlechter
|
||||
als keine.
|
||||
|
||||
### Derselbe Neustart-Fehler, zum zweiten Mal
|
||||
|
||||
Beim Ausliefern fiel im Protokoll auf: `Zündung aus - geparkt seit …` **bei jedem Neustart**. Der
|
||||
Zuhörer las die Registrierung der Entität als Wechsel — exakt die Phantomfahrt aus Abschnitt AX, nur
|
||||
an anderer Stelle. `if alt is None: return` behebt es; der gespeicherte Wert überlebt den Neustart
|
||||
ohnehin. **Merkposten:** jeder neue Zustandszuhörer in diesem Projekt braucht diese Prüfung.
|
||||
|
||||
Live nachgewiesen: Neustart lässt `09:34:15` unverändert · Zündung an → `null` · Zündung aus mit
|
||||
einer Gerätezeit von vor 40 Minuten → `09:04:57` statt `jetzt`.
|
||||
|
||||
Companion-App gleichgezogen (Paritätsregel): `profilAdapter` um `geparktSeit` erweitert,
|
||||
`standortZustand()` braucht die Fahrtenliste nicht mehr. `tsc --noEmit` sauber, 165/165 Tests grün.
|
||||
|
||||
### Genauigkeit hängt am Operanden des Geräts
|
||||
|
||||
Der Zeitpunkt ist nur so genau, wie das Gerät ihn meldet. Stand der Operand des
|
||||
Zündungselements auf **Monitoring** (3), erzeugte ein Wechsel keinen eigenen Datensatz und der
|
||||
Zeitstempel wäre der des nächsten — beim Abstellen also bis zu 900 s zu spät. Der Eigentümer hat
|
||||
Ignition und Movement am 01.09.2026 auf **On Change** gestellt; damit ist der Zeitpunkt exakt.
|
||||
Priorität bleibt Low, es kostet also keine zusätzliche Funkverbindung, nur einen Datensatz mehr in
|
||||
der Warteschlange.
|
||||
|
||||
### Wichtig für die noch offene Streckenrechnung: der GNSS-Zähler driftet im Stand
|
||||
|
||||
Die 0,034 km, die `total_calculated_mileage` kurz nach dem Zurücksetzen zeigte, waren **kein
|
||||
Beleg für Auflösung, sondern GPS-Drift** — der Dongle lag unbewegt auf dem Schreibtisch (Angabe des
|
||||
Eigentümers, 01.09.2026). Static Navigation ist eingeschaltet (`106 = 1`, Quelle `112 = Movement`)
|
||||
und friert die Position ein, offenbar aber nicht den Kilometerzähler.
|
||||
|
||||
**Folge für Abschnitt BA:** die GNSS-Zuwächse dürfen nicht ungefiltert summiert werden. Sie brauchen
|
||||
ein Tor — nur zählen, wenn Bewegung gemeldet ist. Dass `Movement` jetzt auf On Change steht, macht
|
||||
genau dieses Tor erst möglich. Vor der ersten echten Fahrt ist die Größenordnung der Drift unbekannt;
|
||||
sie ist als Erstes zu messen.
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface Fahrzeug {
|
||||
an nur, dass die Zündung an ist. `null` heißt „kein Zündungssensor
|
||||
zugeordnet", also unbekannt — nicht „steht". */
|
||||
zuendung: boolean | null
|
||||
geparktSeit: string | null
|
||||
standortLat: number | null
|
||||
standortLon: number | null
|
||||
standortZeit: string | null
|
||||
@@ -171,6 +172,7 @@ export function profilZuFahrzeug(profil: Profil, status: Fahrzeugstatus): Fahrze
|
||||
gesichert: status.gesichert ?? null,
|
||||
sicherheitscheck: status.sicherheitscheck ?? [],
|
||||
zuendung: status.zuendung ?? null,
|
||||
geparktSeit: (status.geparkt_seit as string | null) ?? null,
|
||||
standortLat: status.standort_lat ?? null,
|
||||
standortLon: status.standort_lon ?? null,
|
||||
standortZeit: status.standort_zeit ?? null,
|
||||
|
||||
@@ -27,6 +27,7 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||
gesichert: null,
|
||||
sicherheitscheck: [],
|
||||
zuendung: null,
|
||||
geparktSeit: null,
|
||||
standortLat: null,
|
||||
standortLon: null,
|
||||
standortZeit: null,
|
||||
|
||||
@@ -112,15 +112,22 @@ function dauerSeitText(ts: number | null): string {
|
||||
damit aber nichts zu tun und bleibt als eigene Zahl stehen. */
|
||||
const PARKPLATZ_AB_MIN = 15
|
||||
|
||||
/* Seit wann das Fahrzeug steht: aus dem Backend, hergeleitet aus dem
|
||||
Zeitpunkt, zu dem die Zuendung zuletzt ausging (in der Zeit des Geraets).
|
||||
|
||||
Bis 2026-09-01 stand hier das Ende der juengsten Fahrt aus der Liste. Das
|
||||
war keine Aussage ueber das Parken, sondern ueber die Fahrterkennung: eine
|
||||
verpasste Fahrt laesst das Fahrzeug seit Tagen stehen, obwohl es gestern
|
||||
unterwegs war. Real vorgekommen - am 31.08.2026 zeigte die Anzeige
|
||||
"geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden.
|
||||
|
||||
Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt, und das wird
|
||||
auch so angezeigt. Eine falsche Zahl ist schlechter als keine. */
|
||||
function standortZustand(
|
||||
fahrzeug: Fahrzeug,
|
||||
fahrten: readonly { ts_end: string }[],
|
||||
): { faehrt: boolean; unbekannt: boolean; seitTs: number | null; geparkt: boolean } {
|
||||
let neuesteTs: number | null = null
|
||||
for (const t of fahrten) {
|
||||
const ts = new Date(t.ts_end).getTime()
|
||||
if (!Number.isNaN(ts) && (neuesteTs === null || ts > neuesteTs)) neuesteTs = ts
|
||||
}
|
||||
const roh = fahrzeug.geparktSeit ? new Date(fahrzeug.geparktSeit).getTime() : NaN
|
||||
const neuesteTs: number | null = Number.isNaN(roh) ? null : roh
|
||||
if (fahrzeug.zuendung === true) return { faehrt: true, unbekannt: false, seitTs: null, geparkt: false }
|
||||
const minuten = neuesteTs != null ? (Date.now() - neuesteTs) / 60000 : null
|
||||
return {
|
||||
@@ -241,7 +248,7 @@ export function StandortVorschau({ geheZu }: { geheZu: () => void }) {
|
||||
|
||||
/** Vollbild-Standortansicht — Vorlage: vStandort() im Panel. */
|
||||
export function Standort() {
|
||||
const { fahrzeug, einstellungen, fahrten } = useDaten()
|
||||
const { fahrzeug, einstellungen } = useDaten()
|
||||
const behaelter = useRef<HTMLDivElement | null>(null)
|
||||
const kartenRef = useRef<import("leaflet").Map | null>(null)
|
||||
const fahrzeugMarkerRef = useRef<import("leaflet").Marker | null>(null)
|
||||
@@ -442,7 +449,7 @@ export function Standort() {
|
||||
: (positionsFehler ?? "Abstand wird ermittelt …")
|
||||
const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : ""
|
||||
const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten)
|
||||
const zustand = standortZustand(fahrzeug, fahrten)
|
||||
const zustand = standortZustand(fahrzeug)
|
||||
const kartenlink = fahrzeugPos
|
||||
? `https://www.google.com/maps/dir/?api=1&destination=${fahrzeugPos.lat},${fahrzeugPos.lon}`
|
||||
: null
|
||||
@@ -584,7 +591,7 @@ export function Standort() {
|
||||
? "Fahrzustand unbekannt"
|
||||
: zustand.seitTs != null
|
||||
? `Geparkt seit ${dauerSeitText(zustand.seitTs)}`
|
||||
: "Fahrzeug steht"}
|
||||
: "Fahrzeug steht, Parkdauer unbekannt"}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Sensorzuordnung:
|
||||
# Ölwechsel-Prognose.
|
||||
#
|
||||
# Beim FMM003 hier NICHT den selbst berechneten Gesamtkilometerstand
|
||||
# (*_total_calculated_mileage) zuordnen: der beruht auf GPS-Strecken-
|
||||
# (*_total_calculated_mileage) zuordnen - dafür gibt es GNSS_KM_SENSOR
|
||||
# rechnung statt auf dem Tacho und damit auf einer anderen Zählbasis als
|
||||
# der echte Fahrzeug-Kilometerstand. Wer ihn einträgt, verfälscht alle
|
||||
# drei genannten Auswertungen mit einem inkonsistenten Basiswert. Der vom
|
||||
@@ -60,6 +60,37 @@ class Sensorzuordnung:
|
||||
# Kilometerstand der EU-Data-Act-Integration ist der richtige.
|
||||
KM_SENSOR: str = ""
|
||||
|
||||
# Der geräteeigene Kilometerzähler, wenn er auf GNSS rechnet
|
||||
# (Trip \ Odometer -> Calculation Source = GNSS). Er löst am 01.09.2026
|
||||
# nachgemessen auf **einen Meter** auf (0,034 km direkt nach dem
|
||||
# Zurücksetzen), während der vom CAN gelesene Fahrzeug-Kilometerstand nur
|
||||
# ganze Kilometer hergibt - über die gesamte Aufzeichnung 147 Schritte von
|
||||
# exakt 1,0 km, kein einziger feinerer.
|
||||
#
|
||||
# Gedacht als FEINAUFLÖSUNG, nicht als Ersatz: KM_SENSOR bleibt der Anker,
|
||||
# weil er der Tacho des Fahrzeugs ist und nicht driftet. Dieser hier liefert
|
||||
# die Nachkommastelle dazwischen und verliert Strecke, wo kein GNSS-Empfang
|
||||
# ist. Beides zusammen ergibt eine Strecke auf 100 m genau, die trotzdem am
|
||||
# Fahrzeugwert hängt.
|
||||
#
|
||||
# NOCH OHNE VERBRAUCHER: die Rolle ist angelegt, damit nach der ersten
|
||||
# echten GNSS-Fahrt nur noch gemessen und nicht mehr zugeordnet werden muss.
|
||||
# Solange sie niemand liest, ändert eine Zuordnung nichts.
|
||||
GNSS_KM_SENSOR: str = ""
|
||||
|
||||
# Die Strecke seit dem letzten Datensatz, ebenfalls vom Gerät gerechnet
|
||||
# (Trip \ Odometer -> Mode = "Between records"; steht die Einstellung auf
|
||||
# "Continuous", zählt das Feld stattdessen die laufende Fahrt und die Rolle
|
||||
# ist unbrauchbar - vor dem Zuordnen also nachsehen).
|
||||
#
|
||||
# Wozu zusätzlich zu GNSS_KM_SENSOR: der Gesamtzähler ist die robustere
|
||||
# Quelle für eine Fahrtstrecke, weil ein verlorener Datensatz die Differenz
|
||||
# nicht verfälscht - er zählt weiter. Die Teilstrecken taugen dafür nur,
|
||||
# wenn keiner fehlt. Umgekehrt zeigen sie, WO Strecke verloren ging, und
|
||||
# sind damit die bessere Gegenprobe. Auch diese Rolle hat noch keinen
|
||||
# Verbraucher.
|
||||
GNSS_TEILSTRECKE_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
|
||||
@@ -185,6 +216,16 @@ 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": "GNSS_KM_SENSOR", "label": "Kilometerzähler (GNSS)", "gruppe": "fahrterkennung",
|
||||
"hinweis": "Der geräteeigene Zähler, auf einen Meter genau. Verfeinert die Fahrtstrecke; der Kilometerstand oben bleibt der maßgebliche Wert.",
|
||||
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km"], "liste": False, "pflicht": False,
|
||||
"beispiel": "total_calculated_mileage",
|
||||
"stichworte": ["gnss", "gps", "calculated", "mileage", "kilometer"]},
|
||||
{"key": "GNSS_TEILSTRECKE_SENSOR", "label": "Teilstrecke je Datensatz (GNSS)", "gruppe": "fahrterkennung",
|
||||
"hinweis": "Strecke seit dem letzten Datensatz - Gegenprobe zum Kilometerzähler und Hinweis darauf, wo Strecke fehlt.",
|
||||
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km"], "liste": False, "pflicht": False,
|
||||
"beispiel": "segment_mileage",
|
||||
"stichworte": ["segment", "teilstrecke", "abschnitt", "mileage"]},
|
||||
{"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,
|
||||
|
||||
@@ -194,6 +194,7 @@ function profilZuCar(p, status) {
|
||||
gesichert: status.gesichert,
|
||||
sicherheitscheck: status.sicherheitscheck || [],
|
||||
zuendung: status.zuendung ?? null,
|
||||
geparktSeit: status.geparkt_seit ?? null,
|
||||
standortLat: status.standort_lat,
|
||||
standortLon: status.standort_lon,
|
||||
standortGenauigkeitM: status.standort_genauigkeit_m,
|
||||
@@ -785,9 +786,20 @@ function letzteFahrt() {
|
||||
}
|
||||
return neueste;
|
||||
}
|
||||
/* Seit wann das Fahrzeug steht: aus dem Backend, hergeleitet aus dem
|
||||
Zeitpunkt, zu dem die Zuendung zuletzt ausging (in der Zeit des Geraets).
|
||||
|
||||
Bis 2026-09-01 stand hier das Ende der juengsten Fahrt aus der Liste. Das
|
||||
war keine Aussage ueber das Parken, sondern ueber die Fahrterkennung: eine
|
||||
verpasste Fahrt laesst das Fahrzeug seit Tagen stehen, obwohl es gestern
|
||||
unterwegs war. Real vorgekommen - am 31.08.2026 zeigte die Anzeige
|
||||
"geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden.
|
||||
|
||||
Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt, und das wird
|
||||
auch so angezeigt. Eine falsche Zahl ist schlechter als keine. */
|
||||
function standortZustand() {
|
||||
const t = letzteFahrt();
|
||||
const seitTs = t && t.ts_end ? new Date(t.ts_end).getTime() : null;
|
||||
const roh = CAR.geparktSeit ? new Date(CAR.geparktSeit).getTime() : NaN;
|
||||
const seitTs = Number.isNaN(roh) ? null : roh;
|
||||
if (CAR.zuendung === true) return { faehrt: true, seitTs: null, parkplatz: false };
|
||||
const minuten = seitTs != null ? (Date.now() - seitTs) / 60000 : null;
|
||||
return {
|
||||
@@ -1351,7 +1363,7 @@ function vStandort() {
|
||||
? `<span class="dot"></span><span>Zündung an</span>`
|
||||
: zustand.unbekannt
|
||||
? `${ciSVG(CI.parking)}<span>Fahrzustand unbekannt</span>`
|
||||
: `${ciSVG(CI.parking)}<span>${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht"}</span>`}
|
||||
: `${ciSVG(CI.parking)}<span>${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht, Parkdauer unbekannt"}</span>`}
|
||||
</div>
|
||||
<div class="standortmenu-werte">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS.fuel}</svg>
|
||||
|
||||
@@ -109,6 +109,8 @@ class Koordinator:
|
||||
self.fahrt_start_ts: datetime.datetime | None = None
|
||||
self.tiefststand_pct: float | None = None
|
||||
self.tiefststand_liter: float | None = None
|
||||
# Seit wann das Fahrzeug steht (Zeit des Geraets). None = unbekannt.
|
||||
self.geparkt_seit_ts: datetime.datetime | None = None
|
||||
# Ergebnis des letzten "Auf Update prüfen"/"Update installieren" -
|
||||
# nur auf Tastendruck gesetzt (siehe dienste.py), nie im normalen
|
||||
# Veröffentlichungs-Takt neu berechnet, damit keine automatischen
|
||||
@@ -242,7 +244,42 @@ class Koordinator:
|
||||
neu = ereignis.data["new_state"]
|
||||
return (neu.state if neu else None, alt.state if alt else None)
|
||||
|
||||
async def _zuendung_angezeigt(self, _ereignis: Event[EventStateChangedData]) -> None:
|
||||
async def _zuendung_angezeigt(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
"""Die Zündung dient nur der Anzeige - und hält den Parkbeginn fest.
|
||||
|
||||
"Geparkt seit" hing bis 2026-09-01 am Ende der jüngsten Fahrt in der
|
||||
Liste. Das ist keine Aussage über das Parken, sondern über die
|
||||
Fahrterkennung: eine verpasste Fahrt lässt das Fahrzeug seit Tagen
|
||||
stehen, obwohl es gestern unterwegs war (real vorgekommen am
|
||||
31.08.2026). Der Zeitpunkt, zu dem die Zündung zuletzt ausging, ist
|
||||
die direkte Antwort.
|
||||
|
||||
Mit der Gerätezeit, nicht mit der Ankunft: der Datensatz kommt bei
|
||||
niedriger Priorität erst mit der nächsten Übertragung an, trägt aber
|
||||
seinen eigenen Zeitstempel (siehe verlauf.geraetezeit())."""
|
||||
neu, alt = self._zustaende(ereignis)
|
||||
# Kein Vorzustand heisst: die Entitaet taucht gerade erst auf (Start von
|
||||
# Home Assistant, Neuladen der Integration). Das ist kein Wechsel der
|
||||
# Zuendung. Ohne diese Pruefung stempelte JEDER Neustart den Parkbeginn
|
||||
# neu - live beobachtet beim Ausliefern von 2026.9.1.6 - und die
|
||||
# Parkdauer begann wieder bei null. Der gespeicherte Wert ueberlebt den
|
||||
# Neustart ohnehin (_laufzeit_laden). Dieselbe Pruefung wie in
|
||||
# fahrterkennung.zuendung_geaendert().
|
||||
if alt is None:
|
||||
await self.fahrzeugstatus_veroeffentlichen()
|
||||
return
|
||||
if neu == "off":
|
||||
zustand = ereignis.data["new_state"]
|
||||
seit = await verlauf.geraetezeit(
|
||||
self.hass,
|
||||
self.zuordnung.werte.MELDEZEIT_SENSOR,
|
||||
zustand.last_updated if zustand else None,
|
||||
datetime.datetime.now(datetime.UTC),
|
||||
)
|
||||
await self.geparkt_seit_setzen(seit)
|
||||
_LOGGER.info("Zündung aus - geparkt seit %s", seit.isoformat())
|
||||
elif neu == "on" and self.geparkt_seit_ts is not None:
|
||||
await self.geparkt_seit_setzen(None)
|
||||
await self.fahrzeugstatus_veroeffentlichen()
|
||||
|
||||
async def _fahrtsignal_geaendert(self, ereignis: Event[EventStateChangedData]) -> None:
|
||||
@@ -291,18 +328,31 @@ class Koordinator:
|
||||
self.fahrt_start_ts = None
|
||||
self.tiefststand_pct = gespeichert.get("tiefststand_pct")
|
||||
self.tiefststand_liter = gespeichert.get("tiefststand_liter")
|
||||
roh = gespeichert.get("geparkt_seit_ts")
|
||||
if roh:
|
||||
try:
|
||||
self.geparkt_seit_ts = datetime.datetime.fromisoformat(roh)
|
||||
except ValueError:
|
||||
self.geparkt_seit_ts = None
|
||||
|
||||
async def _laufzeit_sichern(self) -> None:
|
||||
await self._store.async_save({
|
||||
"fahrt_start_ts": self.fahrt_start_ts.isoformat() if self.fahrt_start_ts else None,
|
||||
"tiefststand_pct": self.tiefststand_pct,
|
||||
"tiefststand_liter": self.tiefststand_liter,
|
||||
"geparkt_seit_ts": (
|
||||
self.geparkt_seit_ts.isoformat() if self.geparkt_seit_ts else None
|
||||
),
|
||||
})
|
||||
|
||||
async def fahrt_start_setzen(self, ts: datetime.datetime | None) -> None:
|
||||
self.fahrt_start_ts = ts
|
||||
await self._laufzeit_sichern()
|
||||
|
||||
async def geparkt_seit_setzen(self, ts: datetime.datetime | None) -> None:
|
||||
self.geparkt_seit_ts = ts
|
||||
await self._laufzeit_sichern()
|
||||
|
||||
async def tiefststand_setzen(self, wert: float | None) -> None:
|
||||
self.tiefststand_pct = wert
|
||||
await self._laufzeit_sichern()
|
||||
@@ -359,7 +409,7 @@ class Koordinator:
|
||||
self.setzen(E_ZUORDNUNG, "aktuell", self.zuordnung.aktueller_stand())
|
||||
|
||||
async def fahrzeugstatus_veroeffentlichen(self) -> None:
|
||||
self.setzen(E_FAHRZEUGSTATUS, "aktuell", fahrzeugstatus(self.hass, self.zuordnung.werte))
|
||||
self.setzen(E_FAHRZEUGSTATUS, "aktuell", fahrzeugstatus(self.hass, self.zuordnung.werte, self.geparkt_seit_ts))
|
||||
|
||||
async def beleg_ergebnis_veroeffentlichen(self, daten: dict) -> None:
|
||||
self.setzen(E_BELEG_ERGEBNIS, "aktuell", daten)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.1.4",
|
||||
"version": "2026.9.1.6",
|
||||
"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"],
|
||||
|
||||
@@ -21,6 +21,8 @@ Oberfläche, landet aber nie in der Datenbank.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .einstellungen import POSITIONEN, Sensorzuordnung
|
||||
@@ -188,7 +190,11 @@ def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict]
|
||||
return eintraege
|
||||
|
||||
|
||||
def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
|
||||
def fahrzeugstatus(
|
||||
hass: HomeAssistant,
|
||||
werte: Sensorzuordnung,
|
||||
geparkt_seit: datetime.datetime | None = None,
|
||||
) -> dict:
|
||||
"""Bündelt die live aus Home Assistant gelesenen Fahrzeugwerte
|
||||
frontend-freundlich, damit die Oberfläche keine Entity-IDs kennen muss."""
|
||||
sicherheitscheck = _sicherheitscheck(hass, werte)
|
||||
@@ -240,6 +246,13 @@ def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
|
||||
# Kilometerstand blieb dauerhaft "offen" und das Fahrzeug damit
|
||||
# dauerhaft "fahrend". None bedeutet: kein Zündungssensor zugeordnet.
|
||||
"zuendung": _zu_bool(zustand_oder_none(hass, werte.ZUENDUNG_SENSOR)),
|
||||
# Seit wann das Fahrzeug steht - der Zeitpunkt, zu dem die Zündung
|
||||
# zuletzt ausging, in der Zeit des Geräts. None heißt ausdrücklich
|
||||
# "unbekannt" und ist von den Oberflächen auch so anzuzeigen: die
|
||||
# frühere Herleitung aus der jüngsten Fahrt der Liste sah zwar immer
|
||||
# nach einer Antwort aus, war aber eine über die Fahrterkennung und
|
||||
# nicht über das Parken (siehe koordinator._zuendung_angezeigt).
|
||||
"geparkt_seit": geparkt_seit.isoformat() if geparkt_seit else None,
|
||||
"standort_lat": standort["lat"],
|
||||
"standort_lon": standort["lon"],
|
||||
"standort_genauigkeit_m": standort["genauigkeit_m"],
|
||||
|
||||
Reference in New Issue
Block a user