Ortsnamen aus dem Backend, Verbrauchsfaktor aus dem Tankbeleg, Radzaehler ueber Fahrten
Ortsauflösung serverseitig (geokodierung.py): der Ort steht jetzt in der Fahrt statt im Zwischenspeicher jedes Geräts. Beide Oberflächen fragen nichts mehr ab. Verbrauchskorrektur (verbrauchskorrektur.py): Faktor = Beleg-Liter / Summe der Einzelfahrten, voll zu voll. Statt einer Schranke am Faktor wird der Nenner geprüft - decken die erkannten Fahrten 90-110 % der Tacho-Spanne ab? Ausgelöst beim Schreiben eines Tankvorgangs, nicht erst bei der nächsten Fahrt. Radzähler: summiert die Fahrten des montierten Satzes statt Tacho-Deltas. Der alte Weg hatte Fahrzeugwechsel als gefahrene Strecke verbucht (375.962 km bei einem Tacho von 61.823). Null heißt unbekannt: ein Kilometerstand von 0 oder ein leeres Feld werden zu None normalisiert, statt an sechs Lesestellen als "Tachostand null" zu gelten. Momentanwerte der Sensoren gelten nur für einen Tankvorgang von jetzt. Oberfläche, beide Codebasen: Wertespalte der Fahrtenliste ausgerichtet, Ort -> Ort in "Zuletzt", "Räder" statt "Reifen", "Termin vereinbart" entfernt, Markenlogo 20 % größer, Übersicht-Symbol auf volle Rasterbreite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,42 @@
|
||||
"""Reifenzähler: jeder gefahrene Kilometer wird dem Satz zugerechnet, der zum
|
||||
Zeitpunkt der Fahrt aktiv war.
|
||||
"""Radzaehler: der eingetragene Stand, und jeder gefahrene Kilometer kommt
|
||||
dazu.
|
||||
|
||||
Früher war der Zähler = aktueller Kilometerstand minus fester Startwert je
|
||||
Satz. Das war falsch, sobald einmal zwischen Sommer und Winter umgeschaltet
|
||||
wurde: die Differenz zum Startwert zählt dann alle seither gefahrenen
|
||||
Kilometer mit, auch die auf dem jeweils anderen Satz. Jetzt führt
|
||||
reifen.saetze.<satz>.km selbst den fortlaufenden Gesamtstand, und
|
||||
reifen.referenz_odo_km hält den zuletzt verarbeiteten Kilometerstand fest,
|
||||
gegen den die nächste Änderung als Delta gemessen wird.
|
||||
DIE VORGESCHICHTE, weil sie erklaert, warum es jetzt so einfach aussieht.
|
||||
Erst war der Zaehler "aktueller Kilometerstand minus fester Startwert je
|
||||
Satz" - falsch, sobald einmal umgeschaltet wurde, weil dann alle seither
|
||||
gefahrenen Kilometer mitzaehlten, auch die auf dem anderen Satz. Danach fuehrte
|
||||
das Profil einen eigenen Zaehler fort und `referenz_odo_km` hielt den zuletzt
|
||||
verarbeiteten Kilometerstand, gegen den das naechste Delta gemessen wurde.
|
||||
|
||||
Keine Abhängigkeit von total_increasing oder utility_meter: die Fortschreibung
|
||||
braucht nur den zuletzt bekannten Absolutwert des Kilometerstand-Sensors. Ein
|
||||
zeitweise "unavailable" Quellsensor verliert dadurch nichts - referenz_odo_km
|
||||
bleibt einfach stehen, bis wieder ein gültiger Wert kommt, und das nächste
|
||||
Delta wird dann gegen diesen alten Stand berechnet. Auch Kilometer, die
|
||||
während eines HA-Ausfalls gefahren wurden, werden so noch dem damals aktiven
|
||||
Satz zugerechnet.
|
||||
Auch das war falsch, nur unauffaelliger: gezaehlt wurde jedes POSITIVE Delta.
|
||||
Ein Sprung nach unten fiel heraus, einer nach oben nicht - und beim Umstecken
|
||||
des Geraets zwischen zwei Fahrzeugen gibt es beide Richtungen. Am 01.09.2026
|
||||
standen deshalb **375.962 km** auf den Sommerraedern, bei einem Tacho von
|
||||
61.823: der Sprung 61.823 -> 209.177 war als gefahrene Strecke verbucht.
|
||||
|
||||
WARUM JETZT DIE FAHRTEN UND NICHT DER TACHO. Ein Sprungschutz am Delta waere
|
||||
moeglich gewesen (und war kurz gebaut), aber die Fahrten pruefen laengst genau
|
||||
das - Rueckwaertssprunge, unmoegliches Tempo, Mindestdauer -, und sie messen
|
||||
seit der GNSS-Verfeinerung auf 100 m statt auf ganze Kilometer. Am echten
|
||||
Bestand nachgemessen: in den sauberen Bereichen trifft die Fahrtsumme die
|
||||
Tacho-Spanne auf 99 bis 103 %, bei einer Fahrt 6,9 gegen 7 km.
|
||||
|
||||
WAS DAS KOSTET, und der Eigentuemer hat es am 01.09.2026 ausdruecklich
|
||||
akzeptiert: was die Fahrterkennung nicht sieht, landet nie auf den Raedern.
|
||||
Der Tacho ist die Wahrheit des Fahrzeugs und holt sich alles zurueck, eine
|
||||
Fahrtsumme nicht.
|
||||
|
||||
WIE GERECHNET WIRD, in genau der Form, die der Eigentuemer verlangt hat:
|
||||
`saetze.<satz>.km` IST der Stand. Von Hand eingetragen wird er direkt - kein
|
||||
Umweg, kein Sockel, keine Rueckrechnung. Jede Fahrt legt ihre Strecke oben
|
||||
drauf, sobald sie feststeht.
|
||||
|
||||
Damit keine Fahrt zweimal zaehlt, merkt sie sich selbst, was von ihr bereits
|
||||
gutgeschrieben wurde (`reifen_gezaehlt_km`). Verfeinert das Screening die
|
||||
Strecke spaeter noch (7,0 -> 6,896 km ist real vorgekommen), wird nur die
|
||||
Differenz nachgetragen. Fahrten aus der Zeit vor dieser Umstellung tragen
|
||||
keinen Radsatz und bleiben deshalb unberuecksichtigt - ihre Kilometer stecken
|
||||
laengst im gespeicherten Stand.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,7 +46,6 @@ import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .ablage import neue_id
|
||||
from .veroeffentlichung import zustand_oder_none
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .koordinator import Koordinator
|
||||
@@ -40,77 +60,120 @@ SAETZE = ("sommer", "winter")
|
||||
_UEBERNOMMENE_FELDER = ("bild", "mass", "druck_vorne", "druck_hinten")
|
||||
|
||||
|
||||
def aktueller_km(k: Koordinator) -> float | None:
|
||||
try:
|
||||
return float(zustand_oder_none(k.hass, k.zuordnung.werte.KM_SENSOR)) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
def aktiver_satz(profil: dict | None) -> str:
|
||||
"""Welcher Satz gerade montiert ist - der Stempel fuer eine neue Fahrt.
|
||||
|
||||
Kleingeschrieben, weil das Profil den Wert historisch mal so und mal
|
||||
grossgeschrieben fuehrt ("Sommer"), die Schluessel in `saetze` aber immer
|
||||
klein sind.
|
||||
"""
|
||||
satz = str((profil or {}).get("reifen", {}).get("aktiv") or "sommer").lower()
|
||||
return satz if satz in SAETZE else "sommer"
|
||||
|
||||
|
||||
async def fortschreiben(k: Koordinator, neuer_km: float | None, neuer_aktiv: str | None = None) -> None:
|
||||
"""Rechnet zuerst mit dem bisher aktiven Satz ab (Delta seit
|
||||
referenz_odo_km), trägt danach optional einen Satzwechsel ein - in dieser
|
||||
Reihenfolge, damit beim Wechseln selbst keine Kilometer dem neuen Satz
|
||||
zugerechnet werden, die eigentlich noch auf dem alten gefahren wurden."""
|
||||
profil = await k.ablage.profil_lesen()
|
||||
if profil is None:
|
||||
def _aufraeumen(reifen: dict) -> bool:
|
||||
"""Reste der frueheren Delta-Buchhaltung wegwerfen. Sie werden nicht mehr
|
||||
gelesen; stehen zu bleiben waere nur eine Falle fuer den naechsten Leser."""
|
||||
geaendert = False
|
||||
for veraltet in ("referenz_odo_km", "referenz_odo_zeit"):
|
||||
if veraltet in reifen:
|
||||
del reifen[veraltet]
|
||||
geaendert = True
|
||||
return geaendert
|
||||
|
||||
|
||||
async def nachtragen(k: Koordinator, profil: dict | None = None) -> None:
|
||||
"""Schreibt jeder Fahrt ihre Strecke dem montierten Satz gut - genau einmal.
|
||||
|
||||
Der Radsatz steht an der Fahrt selbst, nicht am Zeitpunkt. Ein Satzwechsel
|
||||
braucht deshalb keine Sonderbehandlung: eine Fahrt zaehlt fuer den Satz,
|
||||
der WAEHREND ihr montiert war, auch wenn danach umgeschaltet wurde.
|
||||
|
||||
`reifen_gezaehlt_km` haelt fest, wieviel von dieser Fahrt schon
|
||||
gutgeschrieben ist. Verfeinert das Screening die Strecke spaeter noch,
|
||||
wird nur die Differenz nachgetragen statt der ganzen Fahrt ein zweites
|
||||
Mal. Ohne diesen Merker waere entweder jede Korrektur verloren oder jede
|
||||
Fahrt doppelt gezaehlt."""
|
||||
fahrten = await k.ablage.fahrten_lesen()
|
||||
offen = []
|
||||
for f in fahrten:
|
||||
satz = f.get("reifensatz")
|
||||
strecke = f.get("distance_km")
|
||||
if satz not in SAETZE or strecke is None:
|
||||
continue
|
||||
gezaehlt = float(f.get("reifen_gezaehlt_km") or 0)
|
||||
delta = round(float(strecke) - gezaehlt, 1)
|
||||
if delta:
|
||||
offen.append((f, satz, delta, round(float(strecke), 1)))
|
||||
if not offen:
|
||||
return
|
||||
|
||||
if profil is None:
|
||||
profil = await k.ablage.profil_lesen()
|
||||
if profil is None:
|
||||
return
|
||||
reifen = profil.setdefault("reifen", {})
|
||||
saetze = reifen.setdefault("saetze", {})
|
||||
aktiv = reifen.get("aktiv", "sommer")
|
||||
referenz = reifen.get("referenz_odo_km")
|
||||
_aufraeumen(reifen)
|
||||
|
||||
if referenz is not None and neuer_km is not None:
|
||||
delta = round(neuer_km - referenz, 1)
|
||||
if delta > 0:
|
||||
satz = saetze.setdefault(aktiv, {})
|
||||
satz["km"] = round((satz.get("km") or 0) + delta, 1)
|
||||
if neuer_km is not None:
|
||||
reifen["referenz_odo_km"] = neuer_km
|
||||
if neuer_aktiv is not None:
|
||||
reifen["aktiv"] = neuer_aktiv
|
||||
for fahrt, satz, delta, strecke in offen:
|
||||
eintrag = saetze.setdefault(satz, {})
|
||||
eintrag["km"] = round(float(eintrag.get("km") or 0) + delta, 1)
|
||||
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], {"reifen_gezaehlt_km": strecke})
|
||||
fahrt["reifen_gezaehlt_km"] = strecke
|
||||
_LOGGER.info(
|
||||
"Fahrt %s: %s km auf %s gutgeschrieben, neuer Stand %s km",
|
||||
fahrt["trip_id"], delta, satz, eintrag["km"],
|
||||
)
|
||||
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
|
||||
|
||||
async def wechseln(k: Koordinator, satz: str | None) -> None:
|
||||
"""Umschalten auf den anderen Radsatz.
|
||||
|
||||
Braucht keine Abrechnung vorher: jede Fahrt traegt ihren eigenen Satz, es
|
||||
kann also nichts dem falschen gutgeschrieben werden."""
|
||||
if satz not in SAETZE:
|
||||
_LOGGER.warning("Ungültiger Reifensatz '%s'", satz)
|
||||
_LOGGER.warning("Ungültiger Radsatz %s", satz)
|
||||
return
|
||||
await fortschreiben(k, aktueller_km(k), neuer_aktiv=satz)
|
||||
_LOGGER.info("Aktiver Reifensatz auf '%s' umgeschaltet", satz)
|
||||
profil = await k.ablage.profil_lesen()
|
||||
if profil is None:
|
||||
return
|
||||
profil.setdefault("reifen", {})["aktiv"] = satz
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
_LOGGER.info("Aktiver Radsatz auf %s umgeschaltet", satz)
|
||||
|
||||
|
||||
async def km_setzen(k: Koordinator, satz: str | None, km: object) -> None:
|
||||
"""Manuelle Korrektur des gefahrenen Kilometerstands eines Reifensatzes.
|
||||
"""Laufleistung eines Radsatzes von Hand setzen.
|
||||
|
||||
Überschreibt nur den gespeicherten Zähler selbst - referenz_odo_km (der
|
||||
zuletzt verarbeitete Fahrzeug-Kilometerstand) bleibt unverändert, damit
|
||||
die nächste Fortschreibung weiterhin korrekt von hier aus weiterzählt,
|
||||
statt auf den neuen Wert erneut ein Delta seit dem alten Stand
|
||||
aufzuschlagen."""
|
||||
Der eingetragene Wert IST ab sofort der Stand; jeder weitere gefahrene
|
||||
Kilometer kommt oben drauf. Bereits gutgeschriebene Fahrten tauchen nicht
|
||||
erneut auf - sie tragen ihren Merker."""
|
||||
if satz not in SAETZE:
|
||||
_LOGGER.warning("Ungültiger Reifensatz '%s'", satz)
|
||||
_LOGGER.warning("Ungültiger Radsatz %s", satz)
|
||||
return
|
||||
try:
|
||||
wert = round(float(km), 1) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.warning("Ungültiger km-Wert '%s'", km)
|
||||
_LOGGER.warning("Ungültiger km-Wert %s", km)
|
||||
return
|
||||
if wert < 0:
|
||||
_LOGGER.warning("Negativer km-Wert '%s' abgelehnt", wert)
|
||||
_LOGGER.warning("Negativer km-Wert %s abgelehnt", wert)
|
||||
return
|
||||
|
||||
profil = await k.ablage.profil_lesen()
|
||||
if profil is None:
|
||||
return
|
||||
reifen = profil.setdefault("reifen", {})
|
||||
_aufraeumen(reifen)
|
||||
reifen.setdefault("saetze", {}).setdefault(satz, {})["km"] = wert
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
_LOGGER.info("km-Stand '%s' manuell auf %s gesetzt", satz, wert)
|
||||
_LOGGER.info("Laufleistung %s von Hand auf %s km gesetzt", satz, wert)
|
||||
|
||||
|
||||
async def archivieren(k: Koordinator, satz: str | None) -> None:
|
||||
@@ -118,15 +181,19 @@ async def archivieren(k: Koordinator, satz: str | None) -> None:
|
||||
Modell, DOT, Kommentar) als Übersichtszeile ins Archiv und setzt den
|
||||
laufenden Satz auf einen frischen Stand zurück - km auf 0, Marke/Modell/
|
||||
DOT/Kommentar leer (galten für den alten, jetzt ersetzten Reifen), Größe
|
||||
und Solldruck unverändert übernommen (siehe _UEBERNOMMENE_FELDER)."""
|
||||
und Solldruck unverändert übernommen (siehe _UEBERNOMMENE_FELDER).
|
||||
|
||||
Die Fahrten des alten Satzes tragen ihren Merker und werden dem frischen
|
||||
Satz nicht noch einmal gutgeschrieben."""
|
||||
if satz not in SAETZE:
|
||||
_LOGGER.warning("Ungültiger Reifensatz '%s'", satz)
|
||||
_LOGGER.warning("Ungültiger Radsatz %s", satz)
|
||||
return
|
||||
|
||||
profil = await k.ablage.profil_lesen()
|
||||
if profil is None:
|
||||
return
|
||||
reifen = profil.setdefault("reifen", {})
|
||||
_aufraeumen(reifen)
|
||||
saetze = reifen.setdefault("saetze", {})
|
||||
alt = dict(saetze.get(satz, {}))
|
||||
|
||||
@@ -152,12 +219,11 @@ async def archivieren(k: Koordinator, satz: str | None) -> None:
|
||||
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
_LOGGER.info("Reifensatz '%s' archiviert (%s km), neuer Satz angelegt", satz, archiv[0]["km"])
|
||||
|
||||
_LOGGER.info("Radsatz %s archiviert (%s km), neuer Satz angelegt", satz, archiv[0]["km"])
|
||||
|
||||
async def archiv_aktualisieren(k: Koordinator, id_: str | None, daten: dict) -> None:
|
||||
"""Bearbeitet einen archivierten Reifensatz-Eintrag (Übersichtszeile auf
|
||||
der Reifen-Seite, aufgeklappt -> "Bearbeiten")."""
|
||||
der Räder-Seite, aufgeklappt -> "Bearbeiten")."""
|
||||
if not id_:
|
||||
_LOGGER.warning("reifen_archiv_aktualisieren ohne id aufgerufen")
|
||||
return
|
||||
@@ -167,7 +233,7 @@ async def archiv_aktualisieren(k: Koordinator, id_: str | None, daten: dict) ->
|
||||
archiv = profil.setdefault("reifen", {}).setdefault("archiv", [])
|
||||
eintrag = next((e for e in archiv if e.get("id") == id_), None)
|
||||
if eintrag is None:
|
||||
_LOGGER.warning("Archivierter Reifensatz '%s' nicht gefunden", id_)
|
||||
_LOGGER.warning("Archivierter Radsatz '%s' nicht gefunden", id_)
|
||||
return
|
||||
|
||||
erlaubte_felder = (
|
||||
@@ -180,7 +246,7 @@ async def archiv_aktualisieren(k: Koordinator, id_: str | None, daten: dict) ->
|
||||
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
_LOGGER.info("Archivierter Reifensatz '%s' aktualisiert", id_)
|
||||
_LOGGER.info("Archivierter Radsatz '%s' aktualisiert", id_)
|
||||
|
||||
|
||||
async def archiv_loeschen(k: Koordinator, id_: str | None) -> None:
|
||||
@@ -194,12 +260,12 @@ async def archiv_loeschen(k: Koordinator, id_: str | None) -> None:
|
||||
archiv = reifen.get("archiv", [])
|
||||
uebrig = [e for e in archiv if e.get("id") != id_]
|
||||
if len(uebrig) == len(archiv):
|
||||
_LOGGER.warning("Archivierter Reifensatz '%s' nicht gefunden", id_)
|
||||
_LOGGER.warning("Archivierter Radsatz '%s' nicht gefunden", id_)
|
||||
return
|
||||
reifen["archiv"] = uebrig
|
||||
await k.ablage.profil_schreiben(profil)
|
||||
await k.profil_veroeffentlichen(profil)
|
||||
_LOGGER.info("Archivierter Reifensatz '%s' gelöscht", id_)
|
||||
_LOGGER.info("Archivierter Radsatz '%s' gelöscht", id_)
|
||||
|
||||
|
||||
def zustaende(profil: dict | None) -> dict[str, object]:
|
||||
|
||||
Reference in New Issue
Block a user