Hauptuntersuchung: Ableitung aus Erstzulassung und Wartungsplan, drittes Feld entfernt
Gemeldet: Wartungsplan leer, Erstzulassung 01.03.2025, erwartet 01.03.2028 -
gezeigt wurden drei verschiedene Antworten (Uebersicht 01.08.2026, Mein Audi
"August 2026", Service "kein Eintrag im Wartungsplan").
Ursache war das Profilfeld hauptuntersuchung_faellig ("08/2026"), das die
Ableitung ueberstimmte, in keiner Oberflaeche ein Eingabefeld hatte und im
Backend kein Schema. Drei Bildschirme verarbeiteten denselben Wert
verschieden. Das Feld ist samt Lese- und Schreibweg entfernt; es gibt jetzt
zwei Quellen: Wartungsplan + 24 Monate, sonst Erstzulassung + 36 Monate.
Dabei mitbehoben:
* Die Service-Kachel verschluckte abgeleitete Termine, sobald kein
Wartungsplan-Eintrag dahinterstand.
* monatePlus() in der App verlor einen Tag ueber die Zeitumstellung (drei von
fuenf gemessenen Faellen) - betraf Oelwechsel- und Inspektionsprognose
genauso. Das Panel rechnete dort seit jeher richtig.
* Die App verlangte fuer jeden Wartungsplan-Eintrag eine Kilometerangabe. Eine
Hauptuntersuchung ist rein datumsbasiert; ohne km waere der erste HU-Eintrag
ignoriert worden. Ausserdem nahm das Panel den ersten Treffer der Liste, die
App den juengsten - beide nehmen jetzt den juengsten.
* Monatsgenauigkeit ("03/2025") bleibt auf beiden Seiten erhalten.
Neu: paritaet_hu.test.ts vergleicht die App gegen die aus der Panel-Quelle
herausgeschnittenen Originalfunktionen, 15 Faelle. Gesamt 292 App-Tests, 73
Python-Tests mit 231 Untertests, 0 Tracebacks. Live an der Testinstanz auf
allen drei gemeldeten Bildschirmen geprueft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,19 +91,6 @@ class Sensorzuordnung:
|
||||
# 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
|
||||
# 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
|
||||
@@ -226,13 +213,8 @@ FELDER: list[dict] = [
|
||||
"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,
|
||||
"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.",
|
||||
"hinweis": "on = Zündung an. Für die Anzeige \"fährt/steht\"; sie erkennt zugleich 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"]},
|
||||
|
||||
@@ -29,7 +29,6 @@ from .ablage import neue_id
|
||||
from .veroeffentlichung import zustand_oder_none
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
fahrtsignal,
|
||||
geraetezeit,
|
||||
geraetezeit_plausibel,
|
||||
verlauf_lesen,
|
||||
@@ -52,22 +51,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# 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
|
||||
@@ -83,19 +66,20 @@ NACHLAUF_TOLERANZ_S = 120
|
||||
# 02.09.2026 kippte die Zuendung je einmal in den ersten 25 Sekunden fuer unter
|
||||
# eine Sekunde weg und kam sofort zurueck.
|
||||
#
|
||||
# Der gemeldete Zeitpunkt liegt damit systematisch zu spaet, und zwar in BEIDEN
|
||||
# Wegen: auch das Trip-Ende des Geraets haengt an der verzoegerten Zuendung.
|
||||
# Nachgemessen am 02.09.2026: Zuendung aus 21:43:15, Trip aus 21:58:16 - genau
|
||||
# NACHLAUF_S spaeter. Wer das Trip-Signal als Ausloeser nimmt, muss deshalb
|
||||
# beide Spannen abziehen.
|
||||
# Der gemeldete Zeitpunkt liegt damit systematisch zu spaet und wird abgezogen.
|
||||
#
|
||||
# (Bis zum 05.09.2026 gab es einen zweiten Weg ueber das Trip-Signal des
|
||||
# Geraets, dessen Ende an derselben verzoegerten Zuendung hing und deshalb noch
|
||||
# einmal 900 s spaeter kam. Das Trip-Szenario ist am Geraet abgeschaltet und
|
||||
# der Sensor aus der App entfernt - siehe AGENTS.md.)
|
||||
#
|
||||
# Der Wert stand bis zum 31.08.2026 auf 0 und seit dem 01.09.2026 09:38 auf
|
||||
# 180 s (aus den Geraetekonfigurationen rekonstruiert). Fahrten aus der Zeit
|
||||
# dazwischen enden 180 s zu spaet - korrigiert wird ab jetzt, rueckwirkend nur
|
||||
# von Hand.
|
||||
#
|
||||
# Wie NACHLAUF_S die Konfiguration des Geraets und bewusst keine Einstellung in
|
||||
# der Oberflaeche.
|
||||
# Die Konfiguration des Geraets (Parameter 400) und bewusst keine Einstellung
|
||||
# in der Oberflaeche.
|
||||
ZUENDUNG_NACHLAUF_S = 180
|
||||
|
||||
# Wie lange nach einem vorlaeufigen Ende ein spaeter eintreffendes Zuendungs-Aus
|
||||
@@ -216,7 +200,7 @@ async def _echtes_ende(
|
||||
) -> datetime.datetime:
|
||||
"""Das Fahrtende ohne den Nachlauf des Geraets.
|
||||
|
||||
Das Trip-Signal endet NACHLAUF_S nach der Zuendung; abgezogen ergibt das den
|
||||
Die Zuendung wird vom Geraet verzoegert gemeldet; 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()).
|
||||
@@ -227,16 +211,11 @@ async def _echtes_ende(
|
||||
anschliessend. Ohne diese Klemme entstuende aus dreissig Sekunden Zuendung
|
||||
eine Viertelstunde Fahrt.
|
||||
|
||||
ZWEI Nachlaeufe, nicht einer. Das Zuendungselement meldet selbst schon
|
||||
verzoegert (ZUENDUNG_NACHLAUF_S), und das Trip-Ende haengt an dieser bereits
|
||||
verzoegerten Zuendung - wer ueber das Trip-Signal kommt, muss deshalb beide
|
||||
Spannen abziehen, wer ueber die Zuendung kommt, nur die eine. Bis zum
|
||||
03.09.2026 wurde nur NACHLAUF_S abgezogen; seit der Ignition OFF Delay am
|
||||
Das Zuendungselement meldet selbst schon verzoegert
|
||||
(ZUENDUNG_NACHLAUF_S); genau diese Spanne wird abgezogen. Bis zum
|
||||
03.09.2026 geschah das nicht, und seit der Ignition OFF Delay am
|
||||
01.09.2026 auf 180 s steht, endeten die Fahrten dadurch 180 s zu spaet."""
|
||||
nachlauf = ZUENDUNG_NACHLAUF_S
|
||||
ueber_trip = bool(k.zuordnung.werte.TRIP_SENSOR)
|
||||
if ueber_trip:
|
||||
nachlauf += NACHLAUF_S
|
||||
if not nachlauf:
|
||||
return signal_ende
|
||||
gerechnet = signal_ende - datetime.timedelta(seconds=nachlauf)
|
||||
@@ -247,8 +226,6 @@ async def _echtes_ende(
|
||||
int((signal_ende - start_ts).total_seconds()), nachlauf,
|
||||
)
|
||||
return start_ts
|
||||
if ueber_trip:
|
||||
await _nachlauf_gegenpruefen(k, start_ts, signal_ende, gerechnet)
|
||||
|
||||
# Nie hinter dem letzten Lebenszeichen des Geraets.
|
||||
#
|
||||
@@ -273,49 +250,6 @@ async def _echtes_ende(
|
||||
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, k.zuordnung.werte.MELDEZEIT_SENSOR
|
||||
)
|
||||
aus = [ts for ts, wert in punkte if wert == "off"]
|
||||
if not aus:
|
||||
return
|
||||
# Der beobachtete Wechsel traegt den Nachlauf des Zuendungselements noch in
|
||||
# sich, das gerechnete Ende nicht mehr - erst ohne ihn meinen beide Zahlen
|
||||
# denselben Zeitpunkt. Ohne diese Zeile meldete die Pruefung seit dem
|
||||
# 01.09.2026 bei JEDER Fahrt eine Abweichung von 180 s.
|
||||
beobachtet = aus[-1] - datetime.timedelta(seconds=ZUENDUNG_NACHLAUF_S)
|
||||
abweichung = abs((beobachtet - 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,
|
||||
@@ -352,7 +286,7 @@ async def zuendung_geaendert(
|
||||
#
|
||||
# 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
|
||||
# Neustarts war. Stand die Zuendung 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.
|
||||
#
|
||||
@@ -459,11 +393,8 @@ async def _signalwechsel(
|
||||
return
|
||||
# Ein neues "aus" ersetzt ein aelteres, das noch wartet.
|
||||
k.warte_ende_ab_abbrechen()
|
||||
# Ob hier noch gewartet wird, entscheidet _pausenzeit_s(): liegt ein
|
||||
# Trip-Signal an, bringt das Geraet seine Pausentoleranz selbst mit,
|
||||
# und eine zweite obendrauf haette zwei getrennte Fahrten verschmolzen.
|
||||
# Loest die Zuendung aus, gibt es diese Toleranz nicht - dann ist die
|
||||
# Wartezeit unsere.
|
||||
# Wie lange gewartet wird, entscheidet _pausenzeit_s() - die
|
||||
# Wartezeit ist unsere, das Geraet bringt keine mit.
|
||||
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 -
|
||||
@@ -474,10 +405,9 @@ async def _signalwechsel(
|
||||
signal_ende.isoformat(), k.fahrt_start_ts.isoformat(),
|
||||
)
|
||||
signal_ende = jetzt
|
||||
# Erst jetzt die Nachlaeufe des Geraets abziehen: signal_ende ist der
|
||||
# Zeitpunkt, zu dem das SIGNAL endete - die verzoegerte Zuendung oder
|
||||
# das noch spaetere Trip-Ende -, nicht der, zu dem das Fahrzeug
|
||||
# abgestellt wurde.
|
||||
# Erst jetzt den Nachlauf des Geraets abziehen: signal_ende ist der
|
||||
# Zeitpunkt, zu dem das SIGNAL endete - die verzoegerte Zuendung -,
|
||||
# nicht der, zu dem das Fahrzeug abgestellt wurde.
|
||||
ende = await _echtes_ende(k, k.fahrt_start_ts, signal_ende)
|
||||
pause_s = await _pausenzeit_s(k)
|
||||
if pause_s:
|
||||
@@ -568,14 +498,13 @@ async def _zuendung_kehrt_zurueck(
|
||||
async def _pausenzeit_s(k: Koordinator) -> int:
|
||||
"""Unsere eigene Wartezeit nach dem Zuendungs-Aus, in Sekunden.
|
||||
|
||||
Null, solange ein Trip-Signal zugeordnet ist: dann wartet das Geraet
|
||||
bereits (Ignition OFF Timeout), und beides zusammen haette aus einer
|
||||
Viertelstunde eine halbe gemacht. Genau daran ist die Wartezeit am
|
||||
31.08.2026 entfallen - sie kommt hier nur fuer den Weg ueber die Zuendung
|
||||
zurueck.
|
||||
So lange darf eine zurueckkehrende Zuendung noch dieselbe Fahrt sein -
|
||||
Tanken, kurze Besorgung. Der Wert kommt aus dem Regler "Fahrt beenden",
|
||||
derselbe, der auch den Schlaf-Timeout des Dongles stellt (flespi.py).
|
||||
|
||||
Gemessen wird er in GERAETEZEIT, nicht an der Wanduhr - siehe
|
||||
_zuendung_kehrt_zurueck().
|
||||
"""
|
||||
if k.zuordnung.werte.TRIP_SENSOR:
|
||||
return 0
|
||||
try:
|
||||
profil = await k.ablage.profil_lesen()
|
||||
wert = (profil.get("einstellungen") or {}).get("fahrten_pausenzeit_min")
|
||||
@@ -812,7 +741,7 @@ async def jetzt_beenden(k: Koordinator) -> bool:
|
||||
_LOGGER.info("\"Fahrt beenden\" gedrueckt, aber es laeuft keine Fahrt")
|
||||
return False
|
||||
start_ts = k.fahrt_start_ts
|
||||
ende = await _letztes_lebenszeichen(k, fahrtsignal(k.zuordnung.werte), start_ts)
|
||||
ende = await _letztes_lebenszeichen(k, k.zuordnung.werte.ZUENDUNG_SENSOR, start_ts)
|
||||
_LOGGER.info(
|
||||
"Fahrt seit %s von Hand beendet, vorlaeufiges Ende %s",
|
||||
start_ts.isoformat(), ende.isoformat(),
|
||||
@@ -839,7 +768,7 @@ async def nach_neustart_fortsetzen(k: Koordinator) -> None:
|
||||
if k.fahrt_start_ts is None:
|
||||
return
|
||||
|
||||
sensor = fahrtsignal(k.zuordnung.werte)
|
||||
sensor = k.zuordnung.werte.ZUENDUNG_SENSOR
|
||||
if zustand_oder_none(k.hass, sensor) == "on":
|
||||
return # fährt noch - der Beobachter übernimmt wie sonst auch
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
WARUM ÜBERHAUPT
|
||||
---------------
|
||||
Zwei Zahlen dieser Integration gehören eigentlich dem Gerät und stehen bei uns
|
||||
nur als Konstante bzw. Einstellung: `NACHLAUF_S` (Trip: Ignition OFF Timeout)
|
||||
und die Wartezeit bis zum Fahrtende. Ändert jemand die Gegenstücke am Dongle,
|
||||
nur als Konstante bzw. Einstellung: die Wartezeit bis zum Fahrtende
|
||||
(`fahrterkennung.ZUENDUNG_NACHLAUF_S` und der Regler). Ändert jemand die Gegenstücke am Dongle,
|
||||
rechnet die Fahrterkennung still weiter mit dem alten Wert - und niemand merkt
|
||||
es. Genau das ist am 01.09.2026 passiert, als `400` von 0 auf 180 ging: seither
|
||||
endeten alle Fahrten 180 s zu spät, gefunden erst zwei Tage später beim
|
||||
@@ -133,7 +133,7 @@ INTERESSANT: dict[str, dict] = {
|
||||
"nummer": "11804",
|
||||
"titel": "Trip: Ignition OFF Timeout",
|
||||
"einheit": "s",
|
||||
"unser": "NACHLAUF_S",
|
||||
"unser": None,
|
||||
},
|
||||
"trip_scenario.start_speed": {
|
||||
"nummer": "11803",
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.26","sha256":"0e65b78a949052abdcb6765a9a08e1526e67bec911657073d35f7158f51ba0e4","bytes":324250,"gebaut":"2026-09-04T23:10:56Z"}
|
||||
{"version":"2026.9.5.3","sha256":"6c9e0c3ef0429fd6ec6264af982494b9c699e8b3483fea145946d991805b0e47","bytes":324399,"gebaut":"2026-09-04T23:51:45Z"}
|
||||
Binary file not shown.
@@ -225,7 +225,6 @@ function profilZuCar(p, status) {
|
||||
finAutomatisch: p.fahrzeug.fin_automatisch !== false,
|
||||
details: p.fahrzeug.details,
|
||||
erstzulassung: p.fahrzeug.erstzulassung,
|
||||
hu: p.fahrzeug.hauptuntersuchung_faellig,
|
||||
odo: status.km ?? 0,
|
||||
odoBekannt: status.km != null,
|
||||
tankPct: status.tankprozent ?? 0,
|
||||
@@ -287,7 +286,6 @@ function configCarZuProfil(vorherigesProfil) {
|
||||
p.fahrzeug.zusatz = CONFIG.fahrzeugzusatz;
|
||||
p.fahrzeug.untertitel = CONFIG.fahrzeuguntertitel;
|
||||
p.fahrzeug.kennzeichen = CONFIG.kennzeichen;
|
||||
p.fahrzeug.hauptuntersuchung_faellig = CAR.hu;
|
||||
p.fahrzeug.fin = CAR.fin;
|
||||
p.fahrzeug.fin_automatisch = CAR.finAutomatisch;
|
||||
p.fahrzeug.erstzulassung = CAR.erstzulassung;
|
||||
@@ -2362,7 +2360,7 @@ function vHome() {
|
||||
const datum = t.kmDatum || t.datum;
|
||||
const hauptwert = restKm !== null
|
||||
? `${de(restKm)}<span class="unit">km</span>`
|
||||
: (datum ? dedat(datum) : "–");
|
||||
: (datum === t.datum ? datumText(t.datum, t.genau) : (datum ? dedat(datum) : "–"));
|
||||
// Ohne Restkilometer steht das Datum selbst im Zahlenfeld - auf Nutzerwunsch
|
||||
// jetzt in derselben Groesse wie die km-Zahl (vorher 30px, damit "15.04.2027"
|
||||
// auf 375px nicht umbricht - am laufenden Panel geprueft, dass es bei 40px
|
||||
@@ -2924,6 +2922,17 @@ function vService() {
|
||||
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
||||
const hatMeldung = fm.km != null || fmDatum;
|
||||
const iv = intervalle(t);
|
||||
/* Ohne Wartungsplan-Eintrag und ohne Fahrzeugmeldung bleibt bei der
|
||||
Hauptuntersuchung trotzdem ein Termin uebrig: der aus der
|
||||
Erstzulassung abgeleitete (siehe hauptuntersuchungsTermin()). Bis zum
|
||||
05.09.2026 hat diese Kachel ihn verschluckt und "kein Eintrag im
|
||||
Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin
|
||||
auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei
|
||||
Antworten. */
|
||||
if (!iv && !hatMeldung && t.datum) return `<div class="termingruppe">
|
||||
<div class="tg-kopf">${kopf}</div>
|
||||
<div class="tg-wert">${datumText(t.datum, t.genau)}</div>
|
||||
</div>`;
|
||||
if (!iv && !hatMeldung) return `<div class="termingruppe">
|
||||
<div class="tg-kopf">${kopf}</div>
|
||||
<div class="tg-wert leer">kein Eintrag im Wartungsplan</div>
|
||||
@@ -3122,27 +3131,56 @@ function gewaehlterTermin() {
|
||||
return auswahl[Math.min(svAuswahl, auswahl.length - 1)];
|
||||
}
|
||||
|
||||
/* Die Hauptuntersuchung hat drei moegliche Quellen, in dieser Reihenfolge:
|
||||
/* Die Hauptuntersuchung hat ZWEI Quellen, in dieser Reihenfolge:
|
||||
|
||||
1. die im Profil EINGETRAGENE Faelligkeit (CAR.hu). Sie steht auf dem
|
||||
Pruefbericht und ist damit die belastbarste Angabe - bis zum
|
||||
04.09.2026 hat das Panel sie gar nicht benutzt, obwohl sie
|
||||
gespeichert war (Befund des Eigentuemers).
|
||||
2. der letzte Servicebuch-Eintrag plus 24 Monate.
|
||||
3. die Erstzulassung plus 24 Monate.
|
||||
1. der letzte Servicebuch-Eintrag plus **24** Monate.
|
||||
2. die Erstzulassung plus **36** Monate.
|
||||
|
||||
Die 36 gegen 24 sind kein Versehen: die erste Hauptuntersuchung eines
|
||||
Neuwagens ist nach drei Jahren faellig, jede weitere nach zwei.
|
||||
|
||||
Es gibt KEINE dritte Quelle mehr. Bis zum 05.09.2026 stand hier zuerst
|
||||
ein Profilfeld `hauptuntersuchung_faellig`, das alles andere ueberstimmt
|
||||
hat. Dieses Feld hatte in keiner der beiden Oberflaechen ein
|
||||
Eingabefeld und im Backend kein Schema - es lag nur als toter Schluessel
|
||||
in fahrzeugprofil.json und war von dort weder zu sehen noch zu aendern.
|
||||
Gemeldet vom Eigentuemer am 05.09.2026: Wartungsplan leer, Erstzulassung
|
||||
01.03.2025, angezeigt wurde trotzdem August 2026 - das war jener tote
|
||||
Schluessel ("08/2026"). Das Feld ist samt Lese- und Schreibweg
|
||||
entfernt; die Ableitung ist die einzige Quelle. */
|
||||
const HU_MONATE_FOLGE = 24;
|
||||
const HU_MONATE_ERSTE = 36;
|
||||
|
||||
/* Der JUENGSTE Hauptuntersuchungs-Eintrag des Wartungsplans.
|
||||
|
||||
Nicht ueber letzterEintrag(): das nimmt mit .find() den ersten Treffer in
|
||||
Listenreihenfolge, waehrend die App nach Datum absteigend sortiert. Bei
|
||||
mehreren Eintraegen haetten beide Seiten verschiedene Termine gezeigt,
|
||||
ohne dass man es der Anzeige ansieht. */
|
||||
function letzteHauptuntersuchung() {
|
||||
let beste = null;
|
||||
let besteZeit = null;
|
||||
for (const e of CAR.service.buch) {
|
||||
if (!e || typeof e.art !== "string" || !e.art.toLowerCase().includes("hauptunter")) continue;
|
||||
const z = datumAusText(e.datum);
|
||||
if (!z) continue;
|
||||
if (besteZeit === null || z.zeit > besteZeit) { beste = e; besteZeit = z.zeit; }
|
||||
}
|
||||
return beste;
|
||||
}
|
||||
|
||||
Zu 3.: das ging vorher NUR mit einer Erstzulassung im Format MM/JJJJ.
|
||||
Ein vollstaendiges Datum (01.03.2025) - eine voellig richtige Eingabe -
|
||||
liess den Termin still leer. datumAusText() nimmt jetzt beide Formen. */
|
||||
function hauptuntersuchungsTermin(bauen) {
|
||||
const gesetzt = datumAusText(CAR.hu);
|
||||
if (gesetzt) {
|
||||
return { art: "Hauptuntersuchung", km: null, datum: gesetzt.zeit, kmDatum: null,
|
||||
zuerst: "zeit", basis: null, ziel: gesetzt.zeit, genau: gesetzt.genau };
|
||||
const eintrag = letzteHauptuntersuchung();
|
||||
if (eintrag) {
|
||||
return bauen("Hauptuntersuchung", eintrag, null, HU_MONATE_FOLGE);
|
||||
}
|
||||
const erst = datumAusText(CAR.erstzulassung);
|
||||
return bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
|
||||
const termin = bauen("Hauptuntersuchung", null, null, HU_MONATE_ERSTE,
|
||||
erst ? erst.zeit : null);
|
||||
// Eine Erstzulassung als "03/2025" nennt keinen Tag. Dann nennt auch der
|
||||
// abgeleitete Termin keinen - "01.03.2028" waere eine Erfindung.
|
||||
if (erst && erst.genau === "monat") termin.genau = "monat";
|
||||
return termin;
|
||||
}
|
||||
|
||||
function termine() {
|
||||
|
||||
@@ -78,7 +78,7 @@ def _meldezeit(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
|
||||
|
||||
def _geraet_id(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
|
||||
register = er.async_get(hass)
|
||||
for eid in (werte.MELDEZEIT_SENSOR, verlauf.fahrtsignal(werte), werte.ZUENDUNG_SENSOR):
|
||||
for eid in (werte.MELDEZEIT_SENSOR, werte.ZUENDUNG_SENSOR):
|
||||
if not eid:
|
||||
continue
|
||||
eintrag = register.async_get(eid)
|
||||
|
||||
@@ -48,7 +48,6 @@ from .tankerkennung import LITER_SCHWELLE, leerer_tankvorgang, schwelle_prozent
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
durchschnitt_kmh,
|
||||
fahrtsignal,
|
||||
ist_gefahren,
|
||||
strecke_waehlen,
|
||||
zaehlerstrecke,
|
||||
@@ -499,7 +498,7 @@ async def importieren(k: Koordinator, start: object, ende: object) -> None:
|
||||
# Rückblick muss dieselbe Uhr benutzen wie der Livepfad.
|
||||
mz = werte.MELDEZEIT_SENSOR
|
||||
verlaeufe = {
|
||||
"zuendung": await verlauf_lesen(k.hass, fahrtsignal(werte), von, bis, mz),
|
||||
"zuendung": await verlauf_lesen(k.hass, werte.ZUENDUNG_SENSOR, von, bis, mz),
|
||||
"km": await verlauf_lesen(k.hass, werte.KM_SENSOR, von, bis, mz),
|
||||
"gnss_km": await verlauf_lesen(k.hass, werte.GNSS_KM_SENSOR, von, bis, mz),
|
||||
"tank": await verlauf_lesen(k.hass, werte.TANK_SENSOR, von, bis, mz),
|
||||
|
||||
@@ -297,13 +297,10 @@ class Koordinator:
|
||||
self._beobachter.clear()
|
||||
|
||||
werte = self.zuordnung.werte
|
||||
# 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)
|
||||
# Die Zuendung loest die Fahrten aus - seit dem 05.09.2026 als
|
||||
# einzige. Vorher konnte ein zugeordneter Trip-Sensor sie
|
||||
# verdraengen; siehe den Kommentar an _fahrtsignal_geaendert().
|
||||
self._beobachten(werte.ZUENDUNG_SENSOR, self._fahrtsignal_geaendert)
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.26",
|
||||
"version": "2026.9.5.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": [
|
||||
|
||||
@@ -203,19 +203,6 @@ 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 als_kilometerstand(wert: object) -> float | None:
|
||||
"""Ein eingegebener Kilometerstand, oder None wenn er unbekannt ist.
|
||||
|
||||
|
||||
@@ -22,11 +22,16 @@ der Reihenfolge vorheriger Aufrufe.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from . import einstellungen
|
||||
from .ablage import Ablage
|
||||
from .einstellungen import FELDER, SCHLUESSEL, STANDARDWERTE, Sensorzuordnung
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ist_leer(wert: object) -> bool:
|
||||
"""Leer heißt: keine Zuordnung. Auch eine Liste, die nur leere Einträge
|
||||
enthält - das Setup-Menü schickt für unbelegte Positionen ["","",...], und
|
||||
@@ -47,8 +52,24 @@ class Zuordnung:
|
||||
|
||||
async def anwenden(self) -> None:
|
||||
"""Setzt für jedes bekannte Feld den wirksamen Wert - den Override,
|
||||
wenn einer hinterlegt ist, sonst den eingebauten Standardwert."""
|
||||
wenn einer hinterlegt ist, sonst den eingebauten Standardwert.
|
||||
|
||||
Räumt dabei einmalig auf: Schlüssel, die der Katalog nicht mehr kennt,
|
||||
werden aus entitaeten.json entfernt. `speichern()` filtert sie schon
|
||||
beim Schreiben heraus, aber eine bestehende Datei behielt sie bis zum
|
||||
nächsten Speichern - und ein Feld, das es nicht mehr gibt, soll auch
|
||||
nicht mehr in den Daten stehen. Angefallen ist das mit dem Entfernen
|
||||
von TRIP_SENSOR am 05.09.2026."""
|
||||
overrides = await self._ablage.zuordnung_lesen()
|
||||
verwaist = [k for k in overrides if k not in SCHLUESSEL]
|
||||
if verwaist:
|
||||
_LOGGER.info(
|
||||
"Zuordnung: %s kennt der Katalog nicht mehr - entfernt",
|
||||
", ".join(sorted(verwaist)),
|
||||
)
|
||||
await self._ablage.zuordnung_schreiben(
|
||||
{k: v for k, v in overrides.items() if k in SCHLUESSEL}
|
||||
)
|
||||
for key in SCHLUESSEL:
|
||||
wert = overrides.get(key)
|
||||
standard = STANDARDWERTE[key]
|
||||
|
||||
Reference in New Issue
Block a user