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