Fahrten bearbeitbar, Inspektionsprognose, Bootstrap-Takt reisst nicht mehr ab

Sechs gemeldete Punkte plus ein Fund beim Nachlesen des Ladevorgangs:

- HECKKLAPPENSCHLOSS_SENSOR ueberall entfernt (einstellungen.py,
  entitaeten.py-Katalog, _sicherheitscheck()). HECKKLAPPE_SENSOR bleibt.
- Bild-Platzhalter in den Einstellungen war halb abgeschnitten: die bare
  .carfix-Regel der Desktop-Container-Query traf auch den Platzhalter-Div,
  dessen inset:0 von der festen Hoehe uebersteuert wurde. Auf .bildbox
  eingegrenzt.
- Inspektion bekommt eine eigene "voraussichtlich am ..."-Zeile (nur Datum).
  oelwechselPrognose() dafuer auf den gemeinsamen Kern servicePrognose()
  zurueckgefuehrt.
- "Neue Fahrt" bietet jetzt alle Felder, die die Einzelfahrt anzeigt, und die
  Einzelfahrt laesst sich bearbeiten - gemeinsames fahrtFelder()/
  fahrtFormularWerte() nach dem Muster von tankFelder(). Backend: Service
  audi_dashboard_fahrt_aktualisieren, profil.fahrt_bearbeiten() (edited_fields
  schuetzt gegen die Automatik, nicht gegen den Nutzer). Die Art-Pille
  speichert jetzt ebenfalls statt nur lokal umzuschalten.
- Titelzeile steht auf dem Handy buendig zu den Kacheln: .back:not(.on) gibt
  seine Breite und den Gap auf allen Breiten frei, .topbar links 16px.
- Ladebildschirm: nachladeAnstossen() und datenLaden() stiegen bei fehlendem
  HASS aus, ohne eine naechste Runde zu planen - genau die Sackgasse, aus der
  bisher nur ein Menueklick half. Beide planen jetzt weiter, der Timer wird
  beim Abhaengen gestoppt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:40:36 +02:00
parent 91dfd4807a
commit 2d76f10c1c
17 changed files with 670 additions and 161 deletions
+75 -2
View File
@@ -122,13 +122,43 @@ def _fahrt_beenden(start_ts, ende_ts):
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
def _handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz):
"""Die von Hand befüllbaren Felder einer Fahrt auf ihre Datensatz-Namen
abgebildet, ohne die leer gelassenen. Der Aufrufer schreibt die Schlüssel
zusätzlich nach edited_fields - das schützt sie vor dem automatischen
Kilometerstand-Screening (§7.2), das sonst später darüberschreiben würde."""
werte = {
"start_address": start_ort or None,
"end_address": ziel_ort or None,
"odo_start": odo_start,
"odo_end": odo_end,
"distance_km": distanz,
}
return {k: v for k, v in werte.items() if v is not None}
def _strecke_ergaenzen(felder):
"""Fehlt die Distanz, ergibt sie sich aus den beiden Kilometerständen -
dieselbe Rechnung wie im Screening (fahrtabschluss_logik)."""
if "distance_km" not in felder and "odo_start" in felder and "odo_end" in felder:
felder["distance_km"] = round(felder["odo_end"] - felder["odo_start"], 1)
return felder
@service
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat"):
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat",
start_ort=None, ziel_ort=None,
odo_start=None, odo_end=None, distanz=None):
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
automatischen Zündungs-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
automatischen Zündungs-Erkennung. Dieselben Felder wie beim Bearbeiten
einer bestehenden Fahrt (siehe audi_dashboard_fahrt_aktualisieren); alles
außer Start- und Endzeitpunkt ist optional und bleibt leer, wenn es das
Screening später selbst ergänzen soll. Aufruf als
pyscript.audi_dashboard_fahrt_manuell_anlegen."""
start_ts = datetime.datetime.fromisoformat(ts_start)
ende_ts = datetime.datetime.fromisoformat(ts_end)
dauer_s = int((ende_ts - start_ts).total_seconds())
hand = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
fahrt = {
"trip_id": profil.neue_id("t"),
"ts_start": start_ts.isoformat(),
@@ -152,11 +182,54 @@ def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat
"status": "offen",
"edited_fields": [],
}
fahrt.update(hand)
fahrt["edited_fields"] = sorted(hand)
if fahrt["distance_km"] is not None:
fahrt["km_quelle"] = "manuell"
fahrt["status"] = "vollständig"
profil.fahrt_anhaengen(fahrt)
frontend_veroeffentlichung.fahrten_veroeffentlichen()
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
@service
def audi_dashboard_fahrt_aktualisieren(trip_id=None, ts_start=None, ts_end=None, art=None,
start_ort=None, ziel_ort=None,
odo_start=None, odo_end=None, distanz=None):
"""Bearbeitet eine bestehende Fahrt (Einzelfahrt-Seite), egal ob sie
automatisch erkannt oder von Hand angelegt wurde. Aufruf als
pyscript.audi_dashboard_fahrt_aktualisieren."""
if not trip_id:
log.warning("audi_dashboard: fahrt_aktualisieren ohne trip_id aufgerufen")
return
aenderungen = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
if art:
aenderungen["art"] = art
if ts_start and ts_end:
start_ts = datetime.datetime.fromisoformat(ts_start)
ende_ts = datetime.datetime.fromisoformat(ts_end)
aenderungen["ts_start"] = start_ts.isoformat()
aenderungen["ts_end"] = ende_ts.isoformat()
aenderungen["duration_s"] = int((ende_ts - start_ts).total_seconds())
if aenderungen.get("distance_km") is not None:
aenderungen["km_quelle"] = "manuell"
aenderungen["status"] = "vollständig"
# Von Hand gesetzte Felder bleiben von Hand gesetzt: die Liste wächst mit,
# damit ein späteres Screening sie nicht wieder überschreibt.
# Bewusst eine Listen-Abstraktion statt next(<Generator>): pyscript kann
# keine Generator-Ausdrücke auswerten ("not implemented ast
# ast_generatorexp"), Listen-Abstraktionen dagegen schon.
passend = [f for f in profil.fahrten_lesen() if f.get("trip_id") == trip_id]
handfelder = {"start_address", "end_address", "odo_start", "odo_end", "distance_km"}
bisher = set(passend[0].get("edited_fields", [])) if passend else set()
aenderungen["edited_fields"] = sorted(bisher | (handfelder & set(aenderungen)))
if profil.fahrt_bearbeiten(trip_id, aenderungen):
frontend_veroeffentlichung.fahrten_veroeffentlichen()
log.info(f"audi_dashboard: Fahrt {trip_id} aktualisiert")
else:
log.warning(f"audi_dashboard: Fahrt {trip_id} nicht gefunden")
@service
def audi_dashboard_fahrt_loeschen(trip_id=None):
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
@@ -67,7 +67,6 @@ FENSTER_SENSOREN = []
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
HECKKLAPPE_SENSOR = ""
HECKKLAPPENSCHLOSS_SENSOR = ""
HAUBE_SENSOR = ""
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
@@ -85,10 +85,6 @@ FELDER = [
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
{"key": "HECKKLAPPENSCHLOSS_SENSOR", "label": "Heckklappenschloss", "gruppe": "sicherheit",
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["heckklappe", "tailgate", "schloss", "lock"]},
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
@@ -138,7 +138,6 @@ def _sicherheitscheck():
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
for label, sensor in [
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
("Motorhaube", einstellungen.HAUBE_SENSOR),
]:
w = zustand_oder_none(sensor)
+14 -3
View File
@@ -138,16 +138,20 @@ def offene_fahrten():
return sorted(offen, key=lambda f: f.get("ts_start", ""))
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben):
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben, schutz=True):
"""Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und
schreibt den gesamten Bestand neu. Manuell geänderte Felder
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive
funktionieren nach demselben Muster."""
funktionieren nach demselben Muster.
schutz=False hebt genau diese Sperre auf - nötig für Eingaben aus der
Oberfläche: edited_fields schützt gegen die automatische Ergänzung, nicht
gegen den Menschen, der das Feld gerade selbst korrigiert."""
geaendert = False
for d in zeilen:
if d.get(id_feld) == id_wert:
geschuetzt = set(d.get("edited_fields", []))
geschuetzt = set(d.get("edited_fields", [])) if schutz else set()
for feld, wert in aenderungen.items():
if feld not in geschuetzt:
d[feld] = wert
@@ -162,6 +166,13 @@ def fahrt_aktualisieren(trip_id, aenderungen):
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben)
def fahrt_bearbeiten(trip_id, aenderungen):
"""Wie fahrt_aktualisieren(), aber für Eingaben aus der Oberfläche: eine
von Hand gesetzte Angabe sticht auch dann, wenn dasselbe Feld schon einmal
von Hand gesetzt wurde (siehe schutz-Parameter oben)."""
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben, schutz=False)
def fahrt_loeschen(trip_id):
fahrten = fahrten_lesen()
uebrig = [f for f in fahrten if f.get("trip_id") != trip_id]