17149937a6
Fünf vom Nutzer angeforderte Erweiterungen in einer Runde:
- Batteriespannungs-Kachel bekommt eine Messwertliste (Datum, Uhrzeit,
Spannung, optional Außentemperatur über den neuen AUSSENTEMP_SENSOR),
erreichbar über ein neues list-s-Symbol in der Diagramm-Kachel. Werte
über AGM_RUHE_MAX_V (12,8 V) sind keine Ruhespannung, sondern
Lichtmaschinenspannung - die Zeile markiert das jetzt mit
"Generatorspannung" statt es unkommentiert als Messwert auszugeben.
- Jede Zeile der neuen Liste wischbar zum Löschen (neuer Dienst
batterieverlauf_loeschen), über dieselbe Wisch-Mechanik wie Fahrten/
Tankvorgänge in beiden Oberflächen.
- Batteriespannungs-Diagramm im Panel war auf großen Bildschirmen (≥860px,
Spaltenlayout) stark in die Breite gezogen (preserveAspectRatio="none"
bei fester Höhe) - gedeckelt wie die dort bereits vorhandenen Popups.
- Statistik-Tab-Symbol in beiden Oberflächen durch das Audi-CI-Symbol
polls-s ersetzt.
- Neue Möglichkeit, einen Reifensatz zu archivieren ("Neue Räder
anlegen"): der aktuelle Stand (km, Marke, Modell, DOT, Maße, Solldruck,
Kommentar) wandert in ein Archiv, der laufende Satz beginnt bei 0 km neu.
Archiv als aufklappbare Übersicht mit voller Bearbeitungsmöglichkeit,
über drei neue, bewusst nicht über profilSchreiben laufende Dienste
(reifen_archivieren/-aktualisieren/-loeschen) - aus demselben Grund wie
reifen_wechseln: kein im Browser gehaltener Stand darf einen
zwischenzeitlich fortgeschriebenen km-Wert überschreiben.
Live im Testcontainer geprüft (erstmals über :18123 statt :8123 erreichbar)
und dabei zwei echte Layout-Fehler gefunden, die kein Compile-/Testlauf
sehen konnte: die Archiv-Eingabefelder waren durch ein fälschlich
verwendetes .mitEinheit (feste 96px-Breite, eigentlich für Zahl+Einheit
gedacht) abgeschnitten ("Continenta" statt "Continental"), und die
Kilometerzahl in der eingeklappten Archiv-Zeile war klein an das Datum
gequetscht statt wie der Satzname lesbar. Beides behoben, live erneut
bestätigt.
Backend: py_compile clean, services.yaml ergänzt. Panel: node --check
clean. companion-app: tsc/Testsuite (146/146)/Build/OTA-Bündel alle grün.
Manifest 2026.8.25.2 → .8, jeder Neustart im Testcontainer sauber.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
218 lines
8.4 KiB
Python
218 lines
8.4 KiB
Python
"""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.<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.
|
|
|
|
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",
|
|
}
|