Verschlafene Kilometer am Fahrtanfang zurueckholen
Der Dongle wacht nach dem Motorstart erst nach 28 s bis 19,5 min auf; was in der Zeit gefahren wird, zeichnet er nicht auf. Die Kilometer holt das Screening jetzt aus dem Endkilometerstand der Vorgaengerin zurueck und haelt in luecke_km fest, wie viel davon betroffen ist. Keine Sperre der GNSS-Verfeinerung, sondern eine Zerlegung: verschlafener Anfang in ganzen Kilometern, aufgezeichneter Rest metergenau. Sonst waere die feine Zahl systematisch zu klein und wuerde bei kleinen Luecken die Korrektur stillschweigend wieder kassieren. Zwei Schutzgitter, beide am Bestand gelernt: LUECKE_MAX_KM = 50 gegen Differenzen, die keine Weckverzoegerung mehr sein koennen, und ein Rueckfall, wenn _vollstaendig() die Werte daraufhin verwerfen wuerde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12336,3 +12336,74 @@ wertlos; entweder auf `link.sheet` warten oder ein zweites Mal messen.
|
||||
Live geprüft (2026.9.6.1, beide Oberflächen): Reichweite 700, Nächster Service
|
||||
300, Kilometerstand 300, geladene Wide-Schnitte nur noch 300 und 700,
|
||||
Reservebalken `rgb(245, 5, 55)`.
|
||||
|
||||
## DE. Verschlafene Kilometer zurückholen (2026.9.6.2)
|
||||
|
||||
Der FMM003 kommt aus dem Deep Sleep nur über seinen Beschleunigungssensor hoch:
|
||||
die eingestellte Zündungsquelle ist die Motordrehzahl, die liegt am CAN, und
|
||||
CAN ist im Schlaf abgeschaltet. Gemessene Weckzeiten vom 18.08. bis 05.09.2026:
|
||||
**28 s bis 1170 s**. Am 05.09. fehlten dadurch **9 Minuten und 6 Kilometer** am
|
||||
Anfang der Rückfahrt — belegt am geräteeigenen GNSS-Wegzähler, der in fünf
|
||||
Stunden um 244 m weiterlief, während das Auto 6 km fuhr.
|
||||
|
||||
Alle geräteseitigen Auswege sind vom Eigentümer geprüft und verworfen:
|
||||
Dauerbetrieb und GPS-Schlaf wegen des Ruhestroms, Periodic Wakeup ebenso, die
|
||||
Empfindlichkeit des Beschleunigungssensors ist am FMM003 nicht einstellbar, und
|
||||
die Bordspannung taugt an diesem Fahrzeug nicht als Zündungsquelle — bei
|
||||
laufendem Motor liegen **87,6 % von 2089 Messwerten** im Spannungsbereich des
|
||||
stehenden Wagens (Rekuperationsmanagement des RS 4).
|
||||
|
||||
### Was jetzt passiert
|
||||
|
||||
`verlauf.aufzeichnungsluecke_km()` — reine Funktion, sieben Tests in
|
||||
`tests/verlauf/test_aufzeichnungsluecke.py`. Zwischen zwei Fahrten steht das
|
||||
Fahrzeug; ist der Startkilometerstand trotzdem höher als der Endstand der
|
||||
Vorgängerin, wurden diese Kilometer gefahren, ohne dass ein Datensatz entstand.
|
||||
`screening._aufzeichnungsluecken_schliessen()` holt sie zurück und schreibt sie
|
||||
nach `luecke_km` (plus `luecke_unsicher`, wenn schon das Ende der Vorgängerin
|
||||
vorläufig war).
|
||||
|
||||
**Keine Sperre der GNSS-Verfeinerung, sondern eine Zerlegung.** Der Eigentümer:
|
||||
„das macht die gesamte Nutzbarkeit des Fahrtenbuchs sehr grob". Richtig ist:
|
||||
|
||||
```
|
||||
Strecke = verschlafener Anfang (ganze km, Tacho)
|
||||
+ aufgezeichneter Rest (metergenau, GNSS-Zähler)
|
||||
```
|
||||
|
||||
Die Summe wird wie sonst gegen den Tacho geprüft. Die Unschärfe steckt damit
|
||||
nur im verschlafenen Stück; Fahrten ohne Lücke bleiben unverändert metergenau.
|
||||
Ohne die Zerlegung wäre die feine Zahl systematisch zu klein — und bei einer
|
||||
kleinen Lücke läge sie noch innerhalb von `GNSS_TOLERANZ_KM` und kassierte die
|
||||
Korrektur stillschweigend wieder ein. Genau dafür gibt es einen Test.
|
||||
|
||||
### Zwei Schutzgitter, beide durch Schaden gelernt
|
||||
|
||||
Der erste Lauf am 06.09.2026 griff sofort daneben und hat **drei Fahrten aus
|
||||
der Testphase beschädigt** (Kilometerstände auf `None`):
|
||||
|
||||
* **`LUECKE_MAX_KM = 50.0`** — die längste gemessene Weckverzögerung sind
|
||||
19,5 min, bei Autobahntempo rund 42 km. Ohne die Grenze fand die Regel
|
||||
1078 km zwischen zwei Fahrten vom 27.08. (dort fehlen Fahrten in der Liste)
|
||||
und 187831 km am 29.08. (Sprung zwischen den Zahlenbändern des Geräts).
|
||||
* **Rückfall, wenn `_vollstaendig()` die Werte verwirft.** Macht die Korrektur
|
||||
die Strecke unplausibel, war die Annahme falsch, dass die beiden Fahrten
|
||||
aufeinanderfolgen — dann bleibt der Bestand, wie er war.
|
||||
|
||||
Die drei beschädigten Fahrten wurden aus `backups/20260901_072541/` von Hand
|
||||
wiederhergestellt. **Merkposten:** eine Regel, die rückwirkend auf den ganzen
|
||||
Bestand läuft, gehört vor dem ersten Lauf gegen den Bestand simuliert, nicht
|
||||
danach repariert.
|
||||
|
||||
### Stand
|
||||
|
||||
Live in `audi_ha_test`, 0 Tracebacks, 303 App-Tests grün. Die Rückfahrt vom
|
||||
05.09. steht jetzt mit **7,922 km statt ~1 km**, Zeile „Nicht aufgezeichnet:
|
||||
6 km am Anfang" in beiden Oberflächen (am gerenderten Panel nachgemessen).
|
||||
|
||||
**Offen, im selben Zug gefunden:** dieselbe Fahrt hat ein **falsches Ende**.
|
||||
Das gepufferte Zündung-Aus kam am 06.09. um 14:28 mit Gerätezeit 23:21:29 an,
|
||||
`_echtes_ende()` hat daraus aber **14:27:25** gemacht — 15 h Dauer, 0,1 km/h
|
||||
Schnitt. Die Umstempelung auf Gerätezeit hat hier nicht gegriffen
|
||||
(`ZUORDNUNG_MAX_S = 1.0`). Das ist dieselbe Familie wie der Befund zu
|
||||
`_letztes_lebenszeichen()` und noch nicht behoben.
|
||||
|
||||
@@ -71,6 +71,10 @@ export interface Fahrt {
|
||||
/** Das Ende steht nur vorlaeufig fest (Knopf "Fahrt beenden", das Geraet
|
||||
hat sein Zuendungs-Aus noch nicht gemeldet). */
|
||||
ende_vorlaeufig?: boolean;
|
||||
/** Kilometer am Anfang der Fahrt, die das Geraet verschlafen hat. */
|
||||
luecke_km?: number | null;
|
||||
/** Die Zuordnung dieser Luecke ist unsicher - siehe screening.py. */
|
||||
luecke_unsicher?: boolean;
|
||||
pausen?: unknown[];
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,23 @@ export function FahrtDetail({ id, geheZu }: { id: string | undefined; geheZu: (n
|
||||
label="Endkilometer"
|
||||
wert={fahrt.odo_end != null ? `${de(fahrt.odo_end)} km` : "noch offen"}
|
||||
/>
|
||||
{/* Der Dongle kommt aus dem Schlaf nur ueber seinen Beschleunigungssensor hoch
|
||||
und braucht dafuer gemessene 28 s bis 19,5 min. Was in dieser Zeit gefahren
|
||||
wird, zeichnet er nicht auf. Die Kilometer holt das Screening aus dem
|
||||
Endkilometerstand der Vorgaengerin zurueck (verlauf.aufzeichnungsluecke_km),
|
||||
die Route ist unwiederbringlich weg. Deshalb steht es hier: eine Fahrt, die
|
||||
auf den ersten Metern keine Spur hat, soll nicht so aussehen, als waere sie
|
||||
vollstaendig aufgezeichnet. */}
|
||||
{fahrt.luecke_km ? (
|
||||
<Wertzeile
|
||||
label="Nicht aufgezeichnet"
|
||||
wert={
|
||||
fahrt.luecke_unsicher
|
||||
? `${de(fahrt.luecke_km)} km am Anfang · Zuordnung unsicher`
|
||||
: `${de(fahrt.luecke_km)} km am Anfang`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Wertzeile label="Dauer" wert={dauer(fahrt.duration_s)} />
|
||||
<Wertzeile
|
||||
label="Ø Geschwindigkeit"
|
||||
|
||||
@@ -120,6 +120,13 @@ def leere_fahrt(
|
||||
"duration_s": int((ende_ts - start_ts).total_seconds()),
|
||||
"distance_km": None,
|
||||
"km_quelle": None,
|
||||
# Kilometer am Anfang der Fahrt, die das Geraet verschlafen hat -
|
||||
# None heisst "keine Luecke bekannt". Siehe
|
||||
# verlauf.aufzeichnungsluecke_km().
|
||||
"luecke_km": None,
|
||||
# True, wenn schon das Ende der Vorgaengerin nur vorlaeufig war - dann
|
||||
# ist die Zuordnung der Luecke zu DIESER Fahrt nicht gesichert.
|
||||
"luecke_unsicher": False,
|
||||
"odo_start": None,
|
||||
"odo_end": None,
|
||||
"avg_speed_kmh": None,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.6.1","sha256":"abd252933364319a0bf8069f258986b419a0d90e681da0ac51805f58af4123c6","bytes":378619,"gebaut":"2026-09-06T11:46:33Z"}
|
||||
{"version":"2026.9.6.2","sha256":"9c9f09af98149f065e1f874c452d71f0ebb5232334a4740a8f2fe272e161a10b","bytes":378683,"gebaut":"2026-09-06T14:47:37Z"}
|
||||
Binary file not shown.
@@ -4639,6 +4639,7 @@ function vTrip(id) {
|
||||
<div class="row"><dt>Ziel</dt><dd>${dezeitpunkt(new Date(t.ts_end))}<small data-fahrtort="ziel">${fahrtOrtText(t.end_address, t.end_lat, t.end_lon)}</small></dd></div>
|
||||
<div class="row"><dt>Startkilometer</dt><dd>${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}</dd></div>
|
||||
<div class="row"><dt>Endkilometer</dt><dd>${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}</dd></div>
|
||||
${t.luecke_km ? `<div class="row"><dt>Nicht aufgezeichnet</dt><dd>${luecketext(t)}</dd></div>` : ""}
|
||||
<div class="row"><dt>Dauer</dt><dd>${dauerText(t.duration_s)}</dd></div>
|
||||
<div class="row"><dt>Ø Geschwindigkeit</dt><dd>${t.avg_speed_kmh != null ? de(t.avg_speed_kmh, 1) + " km/h" : "liegt nicht vor"}</dd></div>
|
||||
<div class="row"><dt>Höchstgeschwindigkeit</dt><dd>${t.vmax_kmh != null ? de(t.vmax_kmh) + " km/h" : "liegt nicht vor"}</dd></div>
|
||||
@@ -4649,6 +4650,18 @@ function vTrip(id) {
|
||||
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
|
||||
}
|
||||
|
||||
/* Der Dongle kommt aus dem Schlaf nur ueber seinen Beschleunigungssensor hoch
|
||||
und braucht dafuer gemessene 28 s bis 19,5 min. Was in dieser Zeit gefahren
|
||||
wird, zeichnet er nicht auf. Die Kilometer holt das Screening aus dem
|
||||
Endkilometerstand der Vorgaengerin zurueck (verlauf.aufzeichnungsluecke_km),
|
||||
die Route ist unwiederbringlich weg. Deshalb steht es hier: eine Fahrt, die
|
||||
auf den ersten Metern keine Spur hat, soll nicht so aussehen, als waere sie
|
||||
vollstaendig aufgezeichnet. */
|
||||
function luecketext(t) {
|
||||
const km = de(t.luecke_km) + " km";
|
||||
return t.luecke_unsicher ? `${km} am Anfang · Zuordnung unsicher` : `${km} am Anfang`;
|
||||
}
|
||||
|
||||
function quad(t, v) { return `<span class="label" style="margin-top:20px">${t}</span>
|
||||
<div class="quad">${v.map((x) => `<div><div class="n">${x[0]}</div><div class="l">${x[1]}</div></div>`).join("")}</div>`; }
|
||||
/* Der Kachelkopf traegt seit der Designpruefung 2026-08-30 denselben
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.6.1",
|
||||
"version": "2026.9.6.2",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
@@ -47,6 +47,7 @@ from .verlauf import (
|
||||
verbrauch_aus_literstaenden,
|
||||
verlauf_lesen,
|
||||
wert_ab,
|
||||
aufzeichnungsluecke_km,
|
||||
wert_bei,
|
||||
)
|
||||
|
||||
@@ -189,6 +190,11 @@ async def durchfuehren(k: Koordinator) -> None:
|
||||
)
|
||||
fahrt["avg_speed_kmh"] = schnitt
|
||||
|
||||
# Vor der Verfeinerung: eine Fahrt mit Aufzeichnungsluecke bekommt hier
|
||||
# ihren richtigen Startkilometerstand, und die Verfeinerung darf sie
|
||||
# danach nicht mehr anfassen.
|
||||
await _aufzeichnungsluecken_schliessen(k, fahrten)
|
||||
|
||||
# Erst nach den Kilometerstaenden: die Verfeinerung braucht die grobe
|
||||
# Strecke als Anker und laeuft nur auf Fahrten, die sie schon haben.
|
||||
gnss_sensor = k.zuordnung.werte.GNSS_KM_SENSOR
|
||||
@@ -338,6 +344,22 @@ async def _gnss_verfeinern(k: Koordinator, gnss_sensor: str, fahrt: dict) -> Non
|
||||
k.hass, gnss_sensor, start - SUCHFENSTER, ende, k.zuordnung.werte.MELDEZEIT_SENSOR
|
||||
)
|
||||
fein = zaehlerstrecke(punkte, start, ende)
|
||||
|
||||
# Bei einer Aufzeichnungsluecke hat der GNSS-Zaehler nur den Teil gesehen,
|
||||
# in dem das Geraet wach war - seine Zahl waere allein also systematisch zu
|
||||
# klein, und bei einer kleinen Luecke laege sie sogar noch innerhalb von
|
||||
# GNSS_TOLERANZ_KM und kassierte die gerade erst geholte Strecke wieder.
|
||||
#
|
||||
# Deshalb keine Sperre, sondern eine Zerlegung: der verschlafene Anfang in
|
||||
# ganzen Kilometern aus dem Tacho, der aufgezeichnete Rest metergenau aus
|
||||
# dem Zaehler. Die Unschaerfe bleibt damit im verschlafenen Stueck statt in
|
||||
# der ganzen Fahrt (Eigentuemer am 06.09.2026: "das macht die gesamte
|
||||
# Nutzbarkeit des Fahrtenbuchs sehr grob"). Die Summe wird unten wie sonst
|
||||
# auch gegen den Tacho geprueft - passt sie nicht, gilt weiter der Tacho.
|
||||
luecke = fahrt.get("luecke_km")
|
||||
if fein is not None and luecke:
|
||||
fein = round(fein + luecke, 3)
|
||||
|
||||
gewaehlt, genommen = strecke_waehlen(grob, fein)
|
||||
if not genommen:
|
||||
if fein is not None:
|
||||
@@ -352,8 +374,9 @@ async def _gnss_verfeinern(k: Koordinator, gnss_sensor: str, fahrt: dict) -> Non
|
||||
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
|
||||
fahrt.update(aenderungen)
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: Strecke auf %s km verfeinert (Kilometerstand sagte %s km)",
|
||||
"Fahrt %s: Strecke auf %s km verfeinert (Kilometerstand sagte %s km)%s",
|
||||
fahrt.get("trip_id"), gewaehlt, grob,
|
||||
f", davon {luecke} km unaufgezeichnet" if luecke else "",
|
||||
)
|
||||
|
||||
|
||||
@@ -509,6 +532,108 @@ async def _vmax_screenen(k: Koordinator, tempo_sensor: str, fahrt: dict) -> None
|
||||
fahrt["vmax_kmh"] = vmax
|
||||
|
||||
|
||||
# Wie viele Kilometer eine Aufzeichnungsluecke hoechstens haben darf, um noch
|
||||
# als solche zu gelten.
|
||||
#
|
||||
# Die Luecke entsteht, waehrend das Geraet nach dem Motorstart noch schlaeft.
|
||||
# Gemessen ueber alle Aufwachvorgaenge vom 18.08. bis 05.09.2026 dauert das
|
||||
# zwischen 28 s und 1170 s; die laengste gemessene Verzoegerung sind also
|
||||
# 19,5 Minuten, bei Autobahntempo rund 42 km. 50 km lassen das durch und
|
||||
# sperren alles, was keine Weckverzoegerung mehr sein kann.
|
||||
#
|
||||
# Ohne diese Grenze griff die Regel am 06.09.2026 sofort daneben: sie fand
|
||||
# 1078 km zwischen zwei Fahrten vom 27.08. (dort fehlen schlicht Fahrten in der
|
||||
# Liste) und 187831 km am 29.08. (dort sprang der Kilometerstand zwischen den
|
||||
# beiden Zahlenbaendern des Geraets, siehe Abschnitt zum Fahrzeugwechsel in
|
||||
# _vollstaendig). Beide Male ist die Differenz echt, aber sie gehoert nicht an
|
||||
# den Anfang der naechsten Fahrt.
|
||||
LUECKE_MAX_KM = 50.0
|
||||
|
||||
|
||||
async def _aufzeichnungsluecken_schliessen(
|
||||
k: Koordinator, fahrten: list[dict]
|
||||
) -> None:
|
||||
"""Holt die Kilometer zurueck, die das Geraet am Anfang einer Fahrt
|
||||
verschlafen hat.
|
||||
|
||||
Das Screening nimmt als Startkilometerstand den zuletzt VOR dem
|
||||
Fahrtbeginn gemeldeten Wert (wert_bei). Beginnt die Fahrt aber erst in dem
|
||||
Moment, in dem sich das Geraet zurueckmeldet, ist dieser Wert schon der
|
||||
Stand NACH den verschlafenen Kilometern - am 05.09.2026 stand dort 21353
|
||||
statt 21347, und die Fahrt haette 1 km statt 7 km bekommen.
|
||||
|
||||
Der ehrliche Anker ist der Endkilometerstand der Vorgaengerin: dazwischen
|
||||
stand das Fahrzeug. Die Strecke stimmt danach auf ganze Kilometer genau -
|
||||
mehr gibt der CAN-Tacho nicht her, und die Route der verschlafenen Minuten
|
||||
ist unwiederbringlich weg. luecke_km haelt fest, wie viel davon betroffen
|
||||
ist, damit die Oberflaeche es zeigen kann und die GNSS-Verfeinerung die
|
||||
Finger davon laesst."""
|
||||
sortiert = sorted(fahrten, key=lambda f: f.get("ts_start") or "")
|
||||
for vorherige, fahrt in zip(sortiert, sortiert[1:]):
|
||||
fehlt = aufzeichnungsluecke_km(
|
||||
vorherige.get("odo_end"), fahrt.get("odo_start")
|
||||
)
|
||||
if fehlt is None:
|
||||
continue
|
||||
if fehlt > LUECKE_MAX_KM:
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: %s km Unterschied zur Vorgaengerin - zu viel fuer "
|
||||
"eine Weckverzoegerung (Grenze %s km), bleibt unangetastet",
|
||||
fahrt.get("trip_id"), fehlt, LUECKE_MAX_KM,
|
||||
)
|
||||
continue
|
||||
# Der Anker ist nur so gut wie das ENDE der Vorgaengerin. Wurde die von
|
||||
# Hand beendet, steht ihr Endkilometerstand auf einem vorlaeufigen Ende
|
||||
# - dann koennen die Kilometer auch ihr gehoeren und nur falsch
|
||||
# zugeschlagen sein. Die Gesamtleistung stimmt in beiden Faellen, die
|
||||
# Aufteilung zwischen zwei Fahrten nicht (Einwand des Eigentuemers,
|
||||
# 06.09.2026). Deshalb wird es vermerkt statt still verrechnet.
|
||||
alt_start = fahrt.get("odo_start")
|
||||
alt_quelle = fahrt.get("km_quelle")
|
||||
fahrt.update({
|
||||
"luecke_km": fehlt,
|
||||
"luecke_unsicher": bool(vorherige.get("ende_vorlaeufig")),
|
||||
"odo_start": vorherige["odo_end"],
|
||||
"km_quelle": "odometer",
|
||||
})
|
||||
aenderungen = _vollstaendig(
|
||||
{
|
||||
"luecke_km": fehlt,
|
||||
"luecke_unsicher": fahrt["luecke_unsicher"],
|
||||
"odo_start": fahrt["odo_start"],
|
||||
"odo_end": fahrt.get("odo_end"),
|
||||
},
|
||||
fahrt,
|
||||
)
|
||||
# Letzte Sicherung: _vollstaendig() verwirft beide Kilometerstaende,
|
||||
# wenn die Strecke daraus unplausibel wird (Rueckwaertssprung oder
|
||||
# unmoegliche Durchschnittsgeschwindigkeit). Passiert das durch UNSERE
|
||||
# Korrektur, war die Annahme falsch, dass die beiden Fahrten wirklich
|
||||
# aufeinanderfolgen - dann darf sie den Bestand nicht schlechter
|
||||
# zuruecklassen, als sie ihn vorgefunden hat. Am 06.09.2026 genau so
|
||||
# eingetreten, an einer Fahrt aus der Testphase vom 27.08.
|
||||
if aenderungen.get("odo_start") is None:
|
||||
fahrt["odo_start"] = alt_start
|
||||
fahrt["km_quelle"] = alt_quelle
|
||||
fahrt["luecke_km"] = None
|
||||
fahrt["luecke_unsicher"] = False
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: %s km Unterschied zur Vorgaengerin ergaeben eine "
|
||||
"unplausible Strecke - Korrektur verworfen, Bestand bleibt",
|
||||
fahrt.get("trip_id"), fehlt,
|
||||
)
|
||||
continue
|
||||
|
||||
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
|
||||
fahrt.update(aenderungen)
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: %s km am Anfang nicht aufgezeichnet - Startkilometer "
|
||||
"von der Vorgaengerin uebernommen (%s statt %s)",
|
||||
fahrt.get("trip_id"), fehlt, vorherige["odo_end"],
|
||||
round(vorherige["odo_end"] + fehlt, 1),
|
||||
)
|
||||
|
||||
|
||||
async def _lueckenlos_verketten(k: Koordinator) -> None:
|
||||
"""Fahrt-Ende(N) == Fahrt-Start(N+1), solange dazwischen nicht gefahren
|
||||
wurde.
|
||||
|
||||
@@ -543,6 +543,41 @@ def strecke_waehlen(
|
||||
return grob, False
|
||||
|
||||
|
||||
def aufzeichnungsluecke_km(
|
||||
vorheriges_ende: float | None, eigener_start: float | None
|
||||
) -> float | None:
|
||||
"""Die Kilometer am Anfang einer Fahrt, die das Geraet nicht mitbekommen hat.
|
||||
|
||||
Zwischen zwei aufeinanderfolgenden Fahrten steht das Fahrzeug, der
|
||||
Kilometerstand aendert sich also nicht - dieselbe Annahme, auf der
|
||||
_lueckenlos_verketten() in screening.py seit jeher beruht. Steht der
|
||||
Startkilometerstand einer Fahrt trotzdem HOEHER als der Endstand ihrer
|
||||
Vorgaengerin, sind diese Kilometer gefahren worden, ohne dass ein einziger
|
||||
Datensatz entstanden ist.
|
||||
|
||||
WARUM ES DAS GIBT: der FMM003 kommt aus dem Deep Sleep nur ueber seinen
|
||||
Beschleunigungssensor hoch. Die eingestellte Zuendungsquelle ist die
|
||||
Motordrehzahl, die liegt am CAN - und CAN ist im Schlaf abgeschaltet
|
||||
(Teltonika: "In deep sleep mode, OBD, CAN and Eventual I/O activity is
|
||||
disabled"). Die Bordspannung taugt an diesem Fahrzeug nicht als Ersatz:
|
||||
das Rekuperationsmanagement des RS 4 laedt in Schueben, und von 2089
|
||||
Messwerten bei laufendem Motor liegen 87,6 % im Spannungsbereich des
|
||||
stehenden Wagens (gemessen 06.09.2026). Gemessene Weckzeiten reichen von
|
||||
39 s bis 19,5 min; am 05.09.2026 fehlten 9 Minuten und 6 Kilometer.
|
||||
|
||||
Der Eigentuemer hat alle geraeteseitigen Auswege ausgeschlossen
|
||||
(Dauerbetrieb und GPS-Schlaf wegen des Ruhestroms, Periodic Wakeup ebenso,
|
||||
die Empfindlichkeit des Beschleunigungssensors ist am FMM003 nicht
|
||||
einstellbar). Die Luecke bleibt also - sie soll aber wenigstens die
|
||||
Streckenlaenge nicht kosten und sichtbar sein.
|
||||
|
||||
Reine Funktion, damit sie ohne Home Assistant pruefbar ist."""
|
||||
if vorheriges_ende is None or eigener_start is None:
|
||||
return None
|
||||
fehlt = round(eigener_start - vorheriges_ende, 1)
|
||||
return fehlt if fehlt > 0 else None
|
||||
|
||||
|
||||
def ist_gefahren(distanz: float | None) -> bool:
|
||||
"""Hat sich das Fahrzeug bewegt?
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Die Kilometer, die der Dongle am Anfang einer Fahrt verschlaeft.
|
||||
|
||||
Der FMM003 kommt aus dem Deep Sleep nur ueber seinen Beschleunigungssensor
|
||||
hoch: die eingestellte Zuendungsquelle ist die Motordrehzahl, die liegt am CAN,
|
||||
und CAN ist im Schlaf abgeschaltet. Gemessene Weckzeiten am 31.08. bis
|
||||
05.09.2026: 28 s, 144 s, 182 s, 484 s, 543 s, 632 s, 1131 s, 1170 s.
|
||||
|
||||
Die Zahlen hier sind gemessen, nicht erfunden - Rueckfahrt Eichstaett -
|
||||
Adelschlag am 05.09.2026: Motorstart 23:04:16 (aus der Motorlaufzeit 543 s im
|
||||
ersten Datensatz), erster Datensatz aber erst 23:13:19. Der Tacho stand vorher
|
||||
auf 21347 und im ersten Datensatz danach bereits auf 21353.
|
||||
|
||||
Aufruf: python3 tests/verlauf/test_aufzeichnungsluecke.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(_HIER)))
|
||||
|
||||
from custom_components.audi_dashboard import verlauf as v # noqa: E402
|
||||
|
||||
|
||||
class Luecke(unittest.TestCase):
|
||||
def test_echter_fall_vom_05_09(self):
|
||||
"""21347 am Ende der Hinfahrt, 21353 im ersten Datensatz der Rueckfahrt."""
|
||||
self.assertEqual(v.aufzeichnungsluecke_km(21347.0, 21353.0), 6.0)
|
||||
|
||||
def test_keine_luecke_ist_none(self):
|
||||
"""Der Normalfall: die Rueckfahrt beginnt, wo die Hinfahrt endete."""
|
||||
self.assertIsNone(v.aufzeichnungsluecke_km(21347.0, 21347.0))
|
||||
|
||||
def test_rueckwaerts_ist_keine_luecke(self):
|
||||
"""Ein niedrigerer Start ist ein Fahrzeugwechsel des Geraets oder ein
|
||||
Ausreisser - jedenfalls keine verschlafene Strecke. _vollstaendig()
|
||||
faengt den Fall an anderer Stelle ab; hier darf er nur nicht als
|
||||
Luecke durchgehen."""
|
||||
self.assertIsNone(v.aufzeichnungsluecke_km(21353.0, 21347.0))
|
||||
|
||||
def test_unbekannte_werte(self):
|
||||
self.assertIsNone(v.aufzeichnungsluecke_km(None, 21353.0))
|
||||
self.assertIsNone(v.aufzeichnungsluecke_km(21347.0, None))
|
||||
self.assertIsNone(v.aufzeichnungsluecke_km(None, None))
|
||||
|
||||
|
||||
class Zerlegung(unittest.TestCase):
|
||||
"""Verschlafener Anfang in ganzen Kilometern, aufgezeichneter Rest
|
||||
metergenau - und die Summe muss den Tacho-Anker weiterhin bestehen.
|
||||
|
||||
Ohne die Zerlegung waere die feine Zahl allein systematisch zu klein, und
|
||||
bei einer kleinen Luecke laege sie sogar noch innerhalb der Toleranz und
|
||||
kassierte die gerade erst geholte Strecke wieder ein. Genau das soll hier
|
||||
nicht passieren."""
|
||||
|
||||
def test_summe_wird_genommen(self):
|
||||
"""6 km verschlafen, 1,428 km aufgezeichnet, Tacho sagt 7 km."""
|
||||
fein = round(1.428 + 6.0, 3)
|
||||
gewaehlt, genommen = v.strecke_waehlen(7.0, fein)
|
||||
self.assertTrue(genommen)
|
||||
self.assertEqual(gewaehlt, 7.428)
|
||||
|
||||
def test_ohne_zerlegung_wuerde_der_tacho_gewinnen(self):
|
||||
"""Zur Abgrenzung: die feine Zahl allein liegt 5,57 km daneben und
|
||||
wird verworfen - die Fahrt bekaeme 7 statt 7,428 km."""
|
||||
gewaehlt, genommen = v.strecke_waehlen(7.0, 1.428)
|
||||
self.assertFalse(genommen)
|
||||
self.assertEqual(gewaehlt, 7.0)
|
||||
|
||||
def test_kleine_luecke_ist_der_gefaehrliche_fall(self):
|
||||
"""Eine Luecke von 1 km: die feine Zahl allein (5,1 gegen 6) laege
|
||||
INNERHALB der Toleranz und wuerde stillschweigend gewinnen - 0,9 km
|
||||
weniger, ohne dass es jemand merkt. Mit Zerlegung stimmt es."""
|
||||
ohne, genommen_ohne = v.strecke_waehlen(6.0, 5.1)
|
||||
self.assertTrue(genommen_ohne, "ohne Zerlegung schluepft der Fehler durch")
|
||||
self.assertEqual(ohne, 5.1)
|
||||
|
||||
mit, genommen_mit = v.strecke_waehlen(6.0, round(5.1 + 1.0, 3))
|
||||
self.assertTrue(genommen_mit)
|
||||
self.assertEqual(mit, 6.1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user