diff --git a/AGENTS.md b/AGENTS.md index 750dfff..0590f64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/companion-app/src/api/types.ts b/companion-app/src/api/types.ts index d6c2c31..7653bdb 100644 --- a/companion-app/src/api/types.ts +++ b/companion-app/src/api/types.ts @@ -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[]; } diff --git a/companion-app/src/screens/FahrtDetail.tsx b/companion-app/src/screens/FahrtDetail.tsx index a80f0c8..46788da 100644 --- a/companion-app/src/screens/FahrtDetail.tsx +++ b/companion-app/src/screens/FahrtDetail.tsx @@ -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 ? ( + + ) : null}
Ziel
${dezeitpunkt(new Date(t.ts_end))}${fahrtOrtText(t.end_address, t.end_lat, t.end_lon)}
Startkilometer
${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}
Endkilometer
${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}
+ ${t.luecke_km ? `
Nicht aufgezeichnet
${luecketext(t)}
` : ""}
Dauer
${dauerText(t.duration_s)}
Ø Geschwindigkeit
${t.avg_speed_kmh != null ? de(t.avg_speed_kmh, 1) + " km/h" : "liegt nicht vor"}
Höchstgeschwindigkeit
${t.vmax_kmh != null ? de(t.vmax_kmh) + " km/h" : "liegt nicht vor"}
@@ -4649,6 +4650,18 @@ function vTrip(id) { `; } +/* 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 `${t}
${v.map((x) => `
${x[0]}
${x[1]}
`).join("")}
`; } /* Der Kachelkopf traegt seit der Designpruefung 2026-08-30 denselben diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 2e50ac4..930aeb4 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -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": [ diff --git a/custom_components/audi_dashboard/screening.py b/custom_components/audi_dashboard/screening.py index 917976b..a8ff68c 100644 --- a/custom_components/audi_dashboard/screening.py +++ b/custom_components/audi_dashboard/screening.py @@ -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. diff --git a/custom_components/audi_dashboard/verlauf.py b/custom_components/audi_dashboard/verlauf.py index 726295d..f156e14 100644 --- a/custom_components/audi_dashboard/verlauf.py +++ b/custom_components/audi_dashboard/verlauf.py @@ -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? diff --git a/tests/verlauf/test_aufzeichnungsluecke.py b/tests/verlauf/test_aufzeichnungsluecke.py new file mode 100644 index 0000000..c4ec382 --- /dev/null +++ b/tests/verlauf/test_aufzeichnungsluecke.py @@ -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)