e1d570992e
Drei zusammengehörige Teile in einem Repository: - homeassistant/ Das fertige, im Einsatz befindliche Home-Assistant-Panel (panel_custom Custom Element + pyscript-Backend). Echte Fahrzeug- und Personendaten (fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl, Tankbelege) bleiben per .gitignore außen vor; die anonymisierte Vorlage fahrzeugprofil.example.json ist mit dabei. - design-system/ Eigenständige React-Komponentenbibliothek (@audi-dash/ui), die die visuelle Sprache des Panels nachbildet - ohne Audi-Markenzeichen und ohne die lizenzierte Hausschrift. Dient als Grundlage für Claude Design. War bis hierher ein eigenes Repository und ist in dieses eingeschmolzen worden. - companion-app/ Datenschicht der neuen App DataMetric360 (iOS/Android via Capacitor, zusätzlich als Iframe im HA-Dashboard). Noch ohne Oberfläche: REST- und WebSocket-Zugriff auf Home Assistant plus Warteschlange für Änderungen ohne Netz. Ersetzt das eingespritzte hass-Objekt, das nur innerhalb des HA-Frontends existiert. Dazu die Projektdokumentation: SPECIFICATION.md (Ist-Stand des Panels), COMPANION_APP_ARCHITECTURE.md (Architekturentscheidungen der neuen App), AUDIT_2026-08-10.md, DESIGN_BRIEF_DATAMETRIC360.md und der ursprüngliche Bauauftrag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
7.6 KiB
Python
185 lines
7.6 KiB
Python
"""Update-Funktion für die Oberfläche ("Update suchen" in Einstellungen).
|
|
|
|
Voraussetzung: einstellungen.UPDATE_REPO_URL zeigt auf ein Git-Repository, in
|
|
das dieses Projekt (pyscript/ + www/, wie mit update.ps1 auf die eigene
|
|
Testinstanz gespielt) gepflegt/gepusht wird - z. B. ein privates GitHub-Repo,
|
|
genau wie es für die HACS-Integration bereits genutzt wird. Ohne eingetragene
|
|
URL meldet "Update suchen" nur "keine Update-Quelle eingerichtet" (§9-
|
|
Fallback), es passiert nichts von selbst.
|
|
|
|
Kopiert wie update.ps1 bewusst NUR Code:
|
|
- pyscript/ komplett (kein Nutzerdaten-Ordner dort)
|
|
- www/*.js, www/*.css, www/badges/*
|
|
Bewusst NICHT angefasst: www/bilder/ (vom Nutzer hochgeladene Fahrzeugfotos,
|
|
siehe bilderverwaltung.py) und alles unter audi_dashboard/ (Fahrzeugprofil,
|
|
Fahrten- und Tankvorgänge-Archive) - ein Update darf niemals echte
|
|
Fahrzeugdaten oder eigene Fotos überschreiben.
|
|
|
|
Vor jeder Installation wird der bisherige Code-Stand unter
|
|
audi_dashboard/code_backups/<Zeitstempel>/ gesichert (gleiches Muster wie
|
|
backup.py für die Nutzerdaten) - kein automatisches Rollback in der
|
|
Oberfläche, aber ein manueller Weg zurück, falls ein Update Probleme macht.
|
|
|
|
git ist im offiziellen Home-Assistant-Container vorhanden (an der
|
|
Testinstanz geprüft) und subprocess ist über allow_all_imports bereits
|
|
freigegeben (siehe configuration_snippet.yaml), deshalb hier statt eines
|
|
selbstgebauten ZIP-Downloads einfach ein flacher Git-Checkout in einen
|
|
eigenen Zwischenordner, getrennt von /config/audi_dashboard (Nutzerdaten)
|
|
und /config/pyscript, /config/www (Live-Code).
|
|
"""
|
|
|
|
import datetime
|
|
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
|
|
import einstellungen
|
|
import profil
|
|
|
|
QUELLE_ORDNER = "/config/.audi_dashboard_update_quelle"
|
|
BACKUP_ORDNER = f"{profil.BASIS}/code_backups"
|
|
LIVE_PYSCRIPT = "/config/pyscript"
|
|
LIVE_WWW = "/config/www"
|
|
CODE_DATEIEN_WWW = ["audi-dashboard-app.js", "audi-dashboard-panel.js", "audi-dashboard.css"]
|
|
|
|
|
|
def _status_veroeffentlichen(daten):
|
|
state.set("pyscript.audi_dashboard_update_status", "aktuell", new_attributes={"daten": daten})
|
|
|
|
|
|
def _git(*args, cwd=None):
|
|
# subprocess.run() direkt, ohne task.executor - genau wie der bestehende
|
|
# PDF-Parser-Aufruf in belegverarbeitung.py (_parser_aufrufen), an der
|
|
# Testinstanz bereits bestätigt funktionsfähig.
|
|
return subprocess.run(
|
|
["git", *args],
|
|
cwd=cwd, capture_output=True, text=True, timeout=60,
|
|
)
|
|
|
|
|
|
def _lokale_version():
|
|
pfad = f"{LIVE_WWW}/audi-dashboard-version.json"
|
|
if not os.path.exists(pfad):
|
|
return None
|
|
try:
|
|
f = task.executor(io.open, pfad, "r")
|
|
inhalt = f.read()
|
|
f.close()
|
|
return json.loads(inhalt).get("version")
|
|
except (ValueError, OSError):
|
|
return None
|
|
|
|
|
|
def _quelle_aktualisieren():
|
|
"""Klont bzw. holt den konfigurierten Branch flach in QUELLE_ORDNER.
|
|
Gibt (erfolgreich, fehlertext) zurück."""
|
|
zweig = einstellungen.UPDATE_BRANCH or "main"
|
|
if not os.path.exists(f"{QUELLE_ORDNER}/.git"):
|
|
os.makedirs(os.path.dirname(QUELLE_ORDNER), exist_ok=True)
|
|
if os.path.exists(QUELLE_ORDNER):
|
|
task.executor(shutil.rmtree, QUELLE_ORDNER)
|
|
r = _git("clone", "--depth", "1", "--branch", zweig, einstellungen.UPDATE_REPO_URL, QUELLE_ORDNER)
|
|
if r.returncode != 0:
|
|
return False, r.stderr.strip() or "git clone fehlgeschlagen"
|
|
return True, None
|
|
r = _git("fetch", "--depth", "1", "origin", zweig, cwd=QUELLE_ORDNER)
|
|
if r.returncode != 0:
|
|
return False, r.stderr.strip() or "git fetch fehlgeschlagen"
|
|
r = _git("reset", "--hard", f"origin/{zweig}", cwd=QUELLE_ORDNER)
|
|
if r.returncode != 0:
|
|
return False, r.stderr.strip() or "git reset fehlgeschlagen"
|
|
return True, None
|
|
|
|
|
|
@service
|
|
def audi_dashboard_update_pruefen():
|
|
"""Prüft, ob unter einstellungen.UPDATE_REPO_URL eine neuere Version
|
|
vorliegt, ohne etwas zu installieren. Aufruf als
|
|
pyscript.audi_dashboard_update_pruefen."""
|
|
jetzt = datetime.datetime.now().isoformat()
|
|
if not einstellungen.UPDATE_REPO_URL:
|
|
_status_veroeffentlichen({"stand": "keine_quelle", "geprueft_um": jetzt})
|
|
return
|
|
ok, fehler = _quelle_aktualisieren()
|
|
if not ok:
|
|
_status_veroeffentlichen({
|
|
"stand": "fehler", "fehler": fehler, "geprueft_um": jetzt,
|
|
"aktuelle_version": _lokale_version(),
|
|
})
|
|
log.warning(f"audi_dashboard: Update-Prüfung fehlgeschlagen: {fehler}")
|
|
return
|
|
quell_version_pfad = f"{QUELLE_ORDNER}/www/audi-dashboard-version.json"
|
|
neue_version = None
|
|
if os.path.exists(quell_version_pfad):
|
|
f = task.executor(io.open, quell_version_pfad, "r")
|
|
inhalt = f.read()
|
|
f.close()
|
|
try:
|
|
neue_version = json.loads(inhalt).get("version")
|
|
except ValueError:
|
|
neue_version = None
|
|
aktuelle_version = _lokale_version()
|
|
verfuegbar = neue_version is not None and aktuelle_version is not None and neue_version > aktuelle_version
|
|
_status_veroeffentlichen({
|
|
"stand": "geprueft", "verfuegbar": verfuegbar,
|
|
"aktuelle_version": aktuelle_version, "neue_version": neue_version,
|
|
"geprueft_um": jetzt,
|
|
})
|
|
log.info(f"audi_dashboard: Update-Prüfung ok - aktuell={aktuelle_version}, verfügbar={neue_version}")
|
|
|
|
|
|
@service
|
|
def audi_dashboard_update_installieren():
|
|
"""Installiert den zuletzt per audi_dashboard_update_pruefen geholten
|
|
Stand aus QUELLE_ORDNER - sichert vorher den bisherigen Code. Aufruf als
|
|
pyscript.audi_dashboard_update_installieren."""
|
|
if not os.path.exists(f"{QUELLE_ORDNER}/.git"):
|
|
log.warning("audi_dashboard: Update-Installation ohne vorherige Prüfung abgelehnt")
|
|
return
|
|
|
|
zeitstempel = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
ziel_backup = f"{BACKUP_ORDNER}/{zeitstempel}"
|
|
os.makedirs(ziel_backup, exist_ok=True)
|
|
if os.path.exists(LIVE_PYSCRIPT):
|
|
task.executor(shutil.copytree, LIVE_PYSCRIPT, f"{ziel_backup}/pyscript")
|
|
for name in CODE_DATEIEN_WWW:
|
|
quelle = f"{LIVE_WWW}/{name}"
|
|
if os.path.exists(quelle):
|
|
task.executor(shutil.copy, quelle, f"{ziel_backup}/{name}")
|
|
|
|
quelle_pyscript = f"{QUELLE_ORDNER}/pyscript"
|
|
if os.path.exists(quelle_pyscript):
|
|
if os.path.exists(LIVE_PYSCRIPT):
|
|
task.executor(shutil.rmtree, LIVE_PYSCRIPT)
|
|
task.executor(shutil.copytree, quelle_pyscript, LIVE_PYSCRIPT)
|
|
|
|
for name in CODE_DATEIEN_WWW:
|
|
quelle = f"{QUELLE_ORDNER}/www/{name}"
|
|
if os.path.exists(quelle):
|
|
task.executor(shutil.copy, quelle, f"{LIVE_WWW}/{name}")
|
|
|
|
quelle_badges = f"{QUELLE_ORDNER}/www/badges"
|
|
if os.path.exists(quelle_badges):
|
|
ziel_badges = f"{LIVE_WWW}/badges"
|
|
if os.path.exists(ziel_badges):
|
|
task.executor(shutil.rmtree, ziel_badges)
|
|
task.executor(shutil.copytree, quelle_badges, ziel_badges)
|
|
|
|
# Version neu schreiben (aktuelle Uhrzeit als Cache-Buster, wie
|
|
# update.ps1) statt den im Quell-Repo eingecheckten Wert blind zu
|
|
# übernehmen - erzwingt bei jedem Browser einen echten Neuabruf von
|
|
# audi-dashboard-app.js/.css (siehe audi-dashboard-panel.js).
|
|
version = int(datetime.datetime.now().timestamp())
|
|
f = task.executor(io.open, f"{LIVE_WWW}/audi-dashboard-version.json", "w")
|
|
f.write(json.dumps({"version": version}))
|
|
f.close()
|
|
|
|
_status_veroeffentlichen({
|
|
"stand": "installiert", "verfuegbar": False,
|
|
"aktuelle_version": version, "neue_version": None,
|
|
"geprueft_um": datetime.datetime.now().isoformat(),
|
|
})
|
|
log.info(f"audi_dashboard: Update installiert (Backup unter {ziel_backup}), neue Version {version}")
|