Files
audi-app/homeassistant/pyscript/updateverwaltung.py
T
tobias e1d570992e Initialer Import: HA-Panel, Design-System, Companion-App
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>
2026-08-11 00:20:05 +02:00

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}")