Batteriespannungs-Messwertliste, Wisch-Löschen, Reifensatz-Archiv, CI-Icons

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 <[email protected]>
This commit is contained in:
2026-08-27 11:44:24 +02:00
co-authored by Claude Sonnet 5
parent 70253e83a5
commit 17149937a6
23 changed files with 1025 additions and 31 deletions
@@ -20,9 +20,11 @@ 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:
@@ -32,6 +34,11 @@ _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:
@@ -106,6 +113,95 @@ async def km_setzen(k: Koordinator, satz: str | None, km: object) -> None:
_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.