d8b12da36d
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS. Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und "pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die Integration beim ersten Start aus ihrer Vorlage an. Drei alte Schwächen sind dabei mit erledigt: - Die Nutzlast landet nicht mehr in der Recorder-Datenbank (_unrecorded_attributes - das kann nur eine echte Entität). - Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher); fiel sie während eines Ausfalls ins Ende, schließt nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt. - Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg. Namensvertrag geändert, beide Oberflächen mitgezogen: pyscript.audi_dashboard_x -> sensor.audi_dashboard_x, pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das Panel liegt in der Integration und kann nicht driften. Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json. Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport, echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112 Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
4.7 KiB
Python
122 lines
4.7 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 logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .veroeffentlichung import zustand_oder_none
|
|
|
|
if TYPE_CHECKING:
|
|
from .koordinator import Koordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
SAETZE = ("sommer", "winter")
|
|
|
|
|
|
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)
|
|
|
|
|
|
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",
|
|
}
|