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 <[email protected]>
This commit is contained in:
2026-09-02 11:28:37 +02:00
co-authored by Claude Opus 5
parent 78a8017e95
commit 2af5c469d6
27 changed files with 1575 additions and 352 deletions
+79 -4
View File
@@ -31,6 +31,7 @@ import datetime
import logging
from typing import TYPE_CHECKING
from . import reifen, verbrauchskorrektur
from .verlauf import (
UNPLAUSIBLE_KMH,
durchschnitt_kmh,
@@ -148,6 +149,24 @@ async def durchfuehren(k: Koordinator) -> None:
for fahrt in ohne_position:
await _position_screenen(k, lat_sensor, lon_sensor, fahrt)
# Erst NACH der Positionsermittlung: eine Fahrt, die gerade eben ihre
# Koordinaten bekommen hat, soll im selben Durchlauf auch ihren Ort
# bekommen und nicht erst beim naechsten.
k.geokodierung.durchlauf_beginnen()
ohne_ort = [
f
for f in fahrten
if (f.get("start_lat") is not None and not f.get("start_stadt"))
or (f.get("end_lat") is not None and not f.get("end_stadt"))
]
# Neueste zuerst: die sieht der Nutzer als naechstes. Ist das Budget
# aufgebraucht, warten die alten auf den naechsten Durchlauf.
ohne_ort.sort(key=lambda f: f.get("ts_start") or "", reverse=True)
for fahrt in ohne_ort:
if k.geokodierung.rest <= 0:
break
await _orte_screenen(k, fahrt)
# Durchschnitt braucht keinen Sensor - er folgt aus Strecke und Dauer,
# die beide schon dastehen. Deshalb vor dem Geschwindigkeitssensor und
# unabhaengig davon, ob einer zugeordnet ist.
@@ -180,11 +199,19 @@ async def durchfuehren(k: Koordinator) -> None:
if tank_liter_sensor:
ohne_verbrauch = [
f for f in fahrten
if f.get("verbrauch_l_100km") is None and f.get("distance_km")
if f.get("verbrauch_roh_l_100km") is None and f.get("distance_km")
]
for fahrt in ohne_verbrauch:
await _verbrauch_screenen(k, tank_liter_sensor, fahrt)
# Zuletzt: die Eichung braucht die Rohwerte aller Fahrten eines
# Betankungsintervalls und ist damit auf alle Schritte davor angewiesen.
await verbrauchskorrektur.anwenden(k)
# Die Laufleistung der Raeder ist die Summe der Fahrten - sie muss also
# nachziehen, sobald eine Strecke dazukommt oder sich aendert.
await reifen.nachtragen(k)
await _lueckenlos_verketten(k)
await k.fahrten_veroeffentlichen()
@@ -324,6 +351,44 @@ async def _position_screenen(
fahrt.update(aenderungen)
async def _orte_screenen(k: Koordinator, fahrt: dict) -> None:
"""Traegt Start- und Zielort nach, sobald die Koordinaten dastehen.
Der Ort gehoert zu den Fahrtdaten, nicht in den Zwischenspeicher jedes
einzelnen Geraets - Begruendung im Kopf von geokodierung.py.
start_address/end_address stehen in fahrterkennung.HANDFELDER: eine von
Hand eingetragene Anschrift ("Zuhause") darf hier nie ueberschrieben
werden. Zwei Sperren dagegen, absichtlich beide: hier wird nur ein
LEERES Feld gefuellt, und fahrt_aktualisieren() laesst ohnehin jedes
Feld in edited_fields unberuehrt.
Der Ortsname (start_stadt) wird auch dann gesetzt, wenn die Anschrift
von Hand stammt - er beschreibt die Koordinate, nicht die Eingabe, und
die Liste braucht ihn zum Ausrichten der Spalten."""
enden = (
("start", fahrt.get("start_lat"), fahrt.get("start_lon")),
("end", fahrt.get("end_lat"), fahrt.get("end_lon")),
)
aenderungen: dict = {}
for seite, lat, lon in enden:
if lat is None or lon is None:
continue
if fahrt.get(f"{seite}_address") and fahrt.get(f"{seite}_stadt"):
continue
ort = await k.geokodierung.aufloesen(lat, lon)
if ort is None:
continue
if not fahrt.get(f"{seite}_address") and ort.get("anschrift"):
aenderungen[f"{seite}_address"] = ort["anschrift"]
if not fahrt.get(f"{seite}_stadt") and ort.get("stadt"):
aenderungen[f"{seite}_stadt"] = ort["stadt"]
if aenderungen:
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
fahrt.update(aenderungen)
async def _verbrauch_screenen(
k: Koordinator, tank_liter_sensor: str, fahrt: dict
) -> None:
@@ -346,9 +411,19 @@ async def _verbrauch_screenen(
verbrauch = verbrauch_aus_literstaenden(
wert_ab(verlauf, start), wert_bei(verlauf, ende), fahrt.get("distance_km")
)
if verbrauch is not None:
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], {"verbrauch_l_100km": verbrauch})
fahrt["verbrauch_l_100km"] = verbrauch
if verbrauch is None:
return
# Der Rohwert wird immer geschrieben, der Anzeigewert nur, solange noch
# kein Faktor vorliegt - sonst ueberschriebe das Screening eine bereits
# an den Betankungen geeichte Zahl mit der ungeeichten.
aenderungen = {"verbrauch_roh_l_100km": verbrauch}
faktor = fahrt.get("verbrauch_faktor")
aenderungen["verbrauch_l_100km"] = (
round(verbrauch * float(faktor), 1) if faktor else verbrauch
)
await k.ablage.fahrt_aktualisieren(fahrt["trip_id"], aenderungen)
fahrt.update(aenderungen)
async def _vmax_screenen(k: Koordinator, tempo_sensor: str, fahrt: dict) -> None: