"""Reifenzähler: jeder gefahrene Kilometer wird dem Satz zugerechnet, der zum Zeitpunkt der Fahrt aktiv war. 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..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. 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. """ from __future__ import annotations import datetime 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 _LOGGER = logging.getLogger(__name__) SAETZE = ("sommer", "winter") # Felder, die bei "Neue Räder anlegen" mit auf den frischen Satz übernommen # werden - Größe und Solldruck ändern sich bei einem gleich montierten Ersatz # typischerweise nicht, anders als Marke/Modell/DOT (siehe archivieren()). _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 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: return reifen = profil.setdefault("reifen", {}) saetze = reifen.setdefault("saetze", {}) aktiv = reifen.get("aktiv", "sommer") referenz = reifen.get("referenz_odo_km") 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 await k.ablage.profil_schreiben(profil) await k.profil_veroeffentlichen(profil) async def wechseln(k: Koordinator, satz: str | None) -> None: if satz not in SAETZE: _LOGGER.warning("Ungültiger Reifensatz '%s'", satz) return await fortschreiben(k, aktueller_km(k), neuer_aktiv=satz) _LOGGER.info("Aktiver Reifensatz auf '%s' umgeschaltet", satz) async def km_setzen(k: Koordinator, satz: str | None, km: object) -> None: """Manuelle Korrektur des gefahrenen Kilometerstands eines Reifensatzes. Ü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.""" if satz not in SAETZE: _LOGGER.warning("Ungültiger Reifensatz '%s'", satz) return try: wert = round(float(km), 1) # type: ignore[arg-type] except (TypeError, ValueError): _LOGGER.warning("Ungültiger km-Wert '%s'", km) return if wert < 0: _LOGGER.warning("Negativer km-Wert '%s' abgelehnt", wert) return profil = await k.ablage.profil_lesen() if profil is None: return reifen = profil.setdefault("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) async def archivieren(k: Koordinator, satz: str | None) -> None: """"Neue Räder anlegen": legt den aktuellen Stand von `satz` (km, Marke, 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).""" if satz not in SAETZE: _LOGGER.warning("Ungültiger Reifensatz '%s'", satz) return profil = await k.ablage.profil_lesen() if profil is None: return reifen = profil.setdefault("reifen", {}) saetze = reifen.setdefault("saetze", {}) alt = dict(saetze.get(satz, {})) archiv = reifen.setdefault("archiv", []) archiv.insert(0, { "id": neue_id("r"), "satz": satz, "ersetzt_am": datetime.date.today().isoformat(), "km": alt.get("km") or 0, "marke": alt.get("marke") or "", "modell": alt.get("modell") or "", "dot": alt.get("dot") or "", "mass": alt.get("mass") or "", "druck_vorne": alt.get("druck_vorne") or "", "druck_hinten": alt.get("druck_hinten") or "", "kommentar": alt.get("kommentar") or "", }) saetze[satz] = { "km": 0, "marke": "", "modell": "", "dot": "", "kommentar": "", **{feld: alt.get(feld) or "" for feld in _UEBERNOMMENE_FELDER}, } 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"]) async def archiv_aktualisieren(k: Koordinator, id_: str | None, daten: dict) -> None: """Bearbeitet einen archivierten Reifensatz-Eintrag (Übersichtszeile auf der Reifen-Seite, aufgeklappt -> "Bearbeiten").""" if not id_: _LOGGER.warning("reifen_archiv_aktualisieren ohne id aufgerufen") return profil = await k.ablage.profil_lesen() if profil is None: return 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_) return erlaubte_felder = ( "ersetzt_am", "km", "marke", "modell", "dot", "mass", "druck_vorne", "druck_hinten", "kommentar", ) for feld in erlaubte_felder: if feld in daten and daten[feld] is not None: eintrag[feld] = daten[feld] await k.ablage.profil_schreiben(profil) await k.profil_veroeffentlichen(profil) _LOGGER.info("Archivierter Reifensatz '%s' aktualisiert", id_) async def archiv_loeschen(k: Koordinator, id_: str | None) -> None: if not id_: _LOGGER.warning("reifen_archiv_loeschen ohne id aufgerufen") return profil = await k.ablage.profil_lesen() if profil is None: return reifen = profil.setdefault("reifen", {}) 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_) return reifen["archiv"] = uebrig await k.ablage.profil_schreiben(profil) await k.profil_veroeffentlichen(profil) _LOGGER.info("Archivierter Reifensatz '%s' gelöscht", id_) def zustaende(profil: dict | None) -> dict[str, object]: """Die drei Kontrollwerte für die eigenen Reifen-Entitäten. Das Frontend liest die Zahlen direkt aus dem veröffentlichten Profil - diese Entitäten sind nur zur Kontrolle in Entwicklerwerkzeuge -> Zustände und für eigene Automationen des Nutzers da.""" reifen = (profil or {}).get("reifen", {}) saetze = reifen.get("saetze", {}) return { "sommer": saetze.get("sommer", {}).get("km"), "winter": saetze.get("winter", {}).get("km"), "aktiv": reifen.get("aktiv") or "unbekannt", }