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>
116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Sicherung von Fahrzeugprofil, Fahrten, Tankvorgängen und Sensor-Zuordnung.
|
|
|
|
Manuell über "Backup jetzt erstellen" in den Einstellungen oder automatisch je
|
|
nach einstellungen.backup_intervall im Fahrzeugprofil (aus/täglich/
|
|
wöchentlich/monatlich) - immer um 04:00 Uhr, wenn eingestellt. Ein einzelner
|
|
täglicher Termin genügt für alle drei Intervalle: wöchentlich läuft nur
|
|
montags, monatlich nur am Monatsersten, täglich jeden Tag.
|
|
|
|
Jede Sicherung ist ein eigener Zeitstempel-Unterordner mit Kopien der vier
|
|
Bestände, keine ZIP-Datei - eine Sicherung, in die man ohne Werkzeug
|
|
hineinschauen und aus der man eine einzelne Datei zurückkopieren kann, ist im
|
|
Ernstfall mehr wert als ein paar gesparte Kilobyte.
|
|
|
|
entitaeten.json gehört ausdrücklich dazu: ohne sie wäre nach einer
|
|
Wiederherstellung die komplette Sensor-Zuordnung aus dem Setup-Menü verloren,
|
|
und weil sie sonst nirgends abgelegt ist, gäbe es nichts zurückzuholen.
|
|
|
|
"Backup exportieren"/"importieren" in der Oberfläche laufen dagegen rein über
|
|
den Browser (aktuell geladenes Profil + Fahrten + Tankvorgänge als eine
|
|
JSON-Datei); wiederherstellen() hier ist das Gegenstück zum Import, das die
|
|
Bestände wieder komplett überschreibt.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from .koordinator import Koordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DATEIEN = (
|
|
"fahrzeugprofil.json",
|
|
"fahrten.jsonl",
|
|
"tankvorgaenge.jsonl",
|
|
"entitaeten.json",
|
|
)
|
|
|
|
|
|
def _kopieren(basis: str, ziel: str) -> str:
|
|
os.makedirs(ziel, exist_ok=True)
|
|
for name in DATEIEN:
|
|
quelle = os.path.join(basis, name)
|
|
if os.path.exists(quelle):
|
|
shutil.copy(quelle, os.path.join(ziel, name))
|
|
return ziel
|
|
|
|
|
|
async def jetzt(k: Koordinator) -> str:
|
|
zeitstempel = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
ziel = os.path.join(k.ablage.backup_ordner, zeitstempel)
|
|
await k.hass.async_add_executor_job(_kopieren, k.ablage.basis, ziel)
|
|
await _vermerken(k)
|
|
_LOGGER.info("Sicherung erstellt unter %s", ziel)
|
|
return ziel
|
|
|
|
|
|
async def _vermerken(k: Koordinator) -> None:
|
|
profil = await k.ablage.profil_lesen()
|
|
if profil is None:
|
|
return
|
|
profil.setdefault("einstellungen", {})["letztes_backup"] = (
|
|
datetime.datetime.now().isoformat()
|
|
)
|
|
await k.ablage.profil_schreiben(profil)
|
|
await k.profil_veroeffentlichen(profil)
|
|
|
|
|
|
async def automatisch(k: Koordinator) -> None:
|
|
"""Der tägliche 04:00-Termin - prüft selbst, ob heute überhaupt eine
|
|
Sicherung fällig ist."""
|
|
profil = await k.ablage.profil_lesen()
|
|
if profil is None:
|
|
return
|
|
intervall = profil.get("einstellungen", {}).get("backup_intervall", "aus")
|
|
heute = datetime.date.today()
|
|
faellig = (
|
|
intervall == "taeglich"
|
|
or (intervall == "woechentlich" and heute.weekday() == 0)
|
|
or (intervall == "monatlich" and heute.day == 1)
|
|
)
|
|
if not faellig:
|
|
return
|
|
ziel = await jetzt(k)
|
|
_LOGGER.info("Automatische Sicherung (%s) erstellt unter %s", intervall, ziel)
|
|
|
|
|
|
def _als_objekt(wert: object) -> object:
|
|
return json.loads(wert) if isinstance(wert, str) else wert
|
|
|
|
|
|
async def wiederherstellen(k: Koordinator, daten: dict) -> None:
|
|
"""Überschreibt Profil, Fahrten, Tankvorgänge und die Sensor-Zuordnung
|
|
komplett.
|
|
|
|
entitaeten_json ist optional: ältere Sicherungen, die vor dem Setup-Menü
|
|
entstanden sind, enthalten den Abschnitt nicht - dann bleibt die aktuelle
|
|
Zuordnung unangetastet, statt sie zu leeren."""
|
|
if daten.get("profil_json"):
|
|
await k.ablage.profil_schreiben(_als_objekt(daten["profil_json"])) # type: ignore[arg-type]
|
|
if daten.get("fahrten_json") is not None:
|
|
await k.ablage.fahrten_schreiben(_als_objekt(daten["fahrten_json"])) # type: ignore[arg-type]
|
|
if daten.get("tankvorgaenge_json") is not None:
|
|
await k.ablage.tankvorgaenge_schreiben(_als_objekt(daten["tankvorgaenge_json"])) # type: ignore[arg-type]
|
|
if daten.get("entitaeten_json"):
|
|
await k.zuordnung.speichern(_als_objekt(daten["entitaeten_json"])) # type: ignore[arg-type]
|
|
await k.trigger_neu_binden()
|
|
await k.alles_veroeffentlichen()
|
|
_LOGGER.info("Sicherung wiederhergestellt")
|