#!/usr/bin/env bash # # DataMetric360 — Installation in Home Assistant # # Richtet das Panel samt Backend auf einer Home-Assistant-Instanz ein und # verdrahtet die Selbstaktualisierung, damit danach alles Weitere über die # Weboberfläche läuft: Sensoren zuordnen, nach Updates suchen, einspielen. # # Wo ausführen: # Am einfachsten direkt auf der HA-Instanz, im Add-on "Terminal & SSH": # bash <(curl -fsSL ) # Alternativ von einem Rechner aus gegen ein eingebundenes config-Verzeichnis: # ./install.sh --ziel /Volumes/config # # Mehrfach ausführbar: bereits vorhandene Fahrzeugdaten werden nie # überschrieben, und der Abschnitt in der configuration.yaml wird beim # erneuten Lauf ersetzt statt ein zweites Mal angehängt. # set -euo pipefail # ------------------------------------------------------------ Voreinstellungen REPO_STANDARD="https://gitea.nothaft.cloud/paul/audi-app.git" BRANCH="main" ZIEL="/config" QUELLE="" TOKEN="" REPO="$REPO_STANDARD" UPDATE_REPO="" # leer = wie --repo PYSCRIPT_INSTALLIEREN="ja" INTERAKTIV="ja" MARKE_START="# >>> DataMetric360 (von install.sh verwaltet) >>>" MARKE_ENDE="# <<< DataMetric360 <<<" # ------------------------------------------------------------------- Ausgabe rot() { printf '\033[31m%s\033[0m\n' "$*"; } gruen(){ printf '\033[32m%s\033[0m\n' "$*"; } grau() { printf '\033[90m%s\033[0m\n' "$*"; } schritt(){ printf '\n\033[1m==> %s\033[0m\n' "$*"; } fehler(){ rot "FEHLER: $*"; exit 1; } hilfe() { cat <<'ENDE' DataMetric360 — Installation in Home Assistant --ziel config-Verzeichnis von Home Assistant (Standard: /config) --von lokale Arbeitskopie benutzen statt zu klonen --repo Repository zum Klonen (Standard: das Projekt-Repository) --update-repo Was als Updatequelle hinterlegt wird. Standard: wie --repo. "aus" hinterlegt nichts - dann gibt es keine Selbstaktualisierung, aber auch keine Zugangsdaten auf der Platte. Siehe ha_install.md, Abschnitt 1b. --branch Branch (Standard: main) --token langlebiges Zugriffstoken für die Kilometerstand-Historie --ohne-pyscript pyscript nicht mitinstallieren (wenn schon vorhanden) --still keine Rückfragen stellen (für unbeaufsichtigte Läufe) --hilfe diese Übersicht Beispiele bash install.sh # auf der HA-Instanz, klont selbst bash install.sh --von ~/audi-app # aus einer lokalen Arbeitskopie bash install.sh --ziel /Volumes/config # gegen ein eingebundenes Laufwerk bash install.sh --repo https://nutzer:token@gitea.example/paul/audi-app.git bash install.sh --von . --update-repo aus # lokal einspielen, ohne Zugangsdaten abzulegen ENDE } while [ $# -gt 0 ]; do case "$1" in --ziel) ZIEL="${2:-}"; shift 2 ;; --von) QUELLE="${2:-}"; shift 2 ;; --repo) REPO="${2:-}"; shift 2 ;; --update-repo) UPDATE_REPO="${2:-}"; shift 2 ;; --branch) BRANCH="${2:-}"; shift 2 ;; --token) TOKEN="${2:-}"; shift 2 ;; --ohne-pyscript) PYSCRIPT_INSTALLIEREN="nein"; shift ;; --still) INTERAKTIV="nein"; shift ;; --hilfe|-h) hilfe; exit 0 ;; *) fehler "Unbekannte Option: $1 (--hilfe zeigt alle)" ;; esac done # --------------------------------------------------------------- Vorprüfungen schritt "Umgebung prüfen" for werkzeug in python3 curl unzip; do command -v "$werkzeug" >/dev/null 2>&1 || fehler "$werkzeug wird gebraucht, ist aber nicht vorhanden." done [ -d "$ZIEL" ] || fehler "Das Zielverzeichnis $ZIEL gibt es nicht. Mit --ziel den richtigen Pfad angeben." [ -w "$ZIEL" ] || fehler "Keine Schreibrechte auf $ZIEL." if [ ! -f "$ZIEL/configuration.yaml" ]; then rot "In $ZIEL liegt keine configuration.yaml." fehler "Das sieht nicht nach einem Home-Assistant-Verzeichnis aus. Pfad mit --ziel prüfen." fi grau " Ziel: $ZIEL" # ------------------------------------------------------------ Quelle besorgen AUFRAEUMEN="" saeubern() { [ -n "$AUFRAEUMEN" ] && rm -rf "$AUFRAEUMEN" || true; } trap saeubern EXIT if [ -n "$QUELLE" ]; then schritt "Lokale Arbeitskopie benutzen" [ -d "$QUELLE/homeassistant" ] || fehler "In $QUELLE gibt es keinen Ordner homeassistant/." HA_QUELLE="$QUELLE/homeassistant" grau " $HA_QUELLE" else schritt "Projekt herunterladen" command -v git >/dev/null 2>&1 || fehler "git wird zum Klonen gebraucht (oder --von benutzen)." AUFRAEUMEN="$(mktemp -d)" if ! git clone --depth 1 --branch "$BRANCH" "$REPO" "$AUFRAEUMEN/quelle" >/dev/null 2>&1; then rot "Klonen von $REPO fehlgeschlagen." grau " Bei einem privaten Repository ein Zugangstoken in die URL aufnehmen, etwa:" grau " --repo https://:@gitea.example/paul/audi-app.git" exit 1 fi HA_QUELLE="$AUFRAEUMEN/quelle/homeassistant" grau " $REPO ($BRANCH)" fi [ -d "$HA_QUELLE/pyscript" ] || fehler "Im Projekt fehlt homeassistant/pyscript/." # ------------------------------------------------------------------- pyscript if [ "$PYSCRIPT_INSTALLIEREN" = "ja" ]; then if [ -d "$ZIEL/custom_components/pyscript" ]; then schritt "pyscript ist bereits vorhanden" grau " $ZIEL/custom_components/pyscript" else schritt "pyscript installieren" VERSION="$(curl -fsSL https://api.github.com/repos/custom-components/pyscript/releases/latest \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["tag_name"])' 2>/dev/null || true)" [ -n "$VERSION" ] || fehler "Konnte die neueste pyscript-Version nicht ermitteln (kein Internet?)." TMPPY="$(mktemp -d)" curl -fsSL -o "$TMPPY/pyscript.zip" \ "https://github.com/custom-components/pyscript/archive/refs/tags/${VERSION}.zip" unzip -oq "$TMPPY/pyscript.zip" -d "$TMPPY" mkdir -p "$ZIEL/custom_components" cp -r "$TMPPY/pyscript-${VERSION}/custom_components/pyscript" "$ZIEL/custom_components/" rm -rf "$TMPPY" gruen " pyscript $VERSION installiert" fi fi # --------------------------------------------------------- Dateien einspielen schritt "Backend und Oberfläche einspielen" mkdir -p "$ZIEL/pyscript" "$ZIEL/www" "$ZIEL/audi_dashboard" # pyscript und www sind Code - vollständig ersetzen. cp -r "$HA_QUELLE/pyscript/." "$ZIEL/pyscript/" cp -r "$HA_QUELLE/www/." "$ZIEL/www/" grau " pyscript/ und www/ aktualisiert" # Der Belegleser liegt in data/, ist aber Code (belegverarbeitung.py ruft ihn # als Unterprozess auf) - deshalb hier mit, anders als die Datenbestände. cp "$HA_QUELLE/data/shell_beleg_parser.py" "$ZIEL/audi_dashboard/shell_beleg_parser.py" # Datenbestände: nur anlegen, was fehlt. Vorhandenes nie überschreiben. if [ ! -f "$ZIEL/audi_dashboard/fahrzeugprofil.json" ]; then cp "$HA_QUELLE/data/fahrzeugprofil.example.json" "$ZIEL/audi_dashboard/fahrzeugprofil.json" gruen " Fahrzeugprofil aus der Vorlage angelegt" else grau " Fahrzeugprofil vorhanden - unangetastet" fi for datei in fahrten.jsonl tankvorgaenge.jsonl; do [ -f "$ZIEL/audi_dashboard/$datei" ] || : > "$ZIEL/audi_dashboard/$datei" done mkdir -p "$ZIEL/audi_dashboard/belege" # --------------------------------------------------- Selbstaktualisierung schritt "Selbstaktualisierung verdrahten" # Was hinterlegt wird, ist bewusst von der Installationsquelle getrennt: die # Selbstaktualisierung laeuft spaeter im Home-Assistant-Prozess und klont # allein anhand dieser URL - Zugangsdaten muessen also darin stehen und landen # damit im Klartext in einstellungen.py. Wer das nicht will, gibt # "--update-repo aus" an und aktualisiert weiterhin ueber dieses Skript. QUELLE_FUER_UPDATE="${UPDATE_REPO:-$REPO}" if [ "$QUELLE_FUER_UPDATE" = "aus" ]; then QUELLE_FUER_UPDATE="" fi python3 - "$ZIEL/pyscript/modules/einstellungen.py" "$QUELLE_FUER_UPDATE" "$BRANCH" <<'PYENDE' import re, sys pfad, repo, branch = sys.argv[1], sys.argv[2], sys.argv[3] with open(pfad, encoding="utf-8") as f: inhalt = f.read() inhalt = re.sub(r'^UPDATE_REPO_URL\s*=.*$', f'UPDATE_REPO_URL = "{repo}"', inhalt, count=1, flags=re.M) inhalt = re.sub(r'^UPDATE_BRANCH\s*=.*$', f'UPDATE_BRANCH = "{branch}"', inhalt, count=1, flags=re.M) with open(pfad, "w", encoding="utf-8") as f: f.write(inhalt) PYENDE if [ -n "$QUELLE_FUER_UPDATE" ]; then grau " Updatequelle: $(printf '%s' "$QUELLE_FUER_UPDATE" | sed -E 's#://[^@/]+@#://***@#') ($BRANCH)" grau " Danach in der App unter Einstellungen → Version → \"Update suchen\"" case "$QUELLE_FUER_UPDATE" in *://*:*@*) rot " Hinweis: Die URL enthaelt Zugangsdaten. Sie stehen im Klartext in" rot " pyscript/modules/einstellungen.py und damit auch in jeder Sicherung." ;; esac else grau " Keine Updatequelle hinterlegt - Aktualisieren laeuft weiter ueber dieses Skript." fi # ---------------------------------------------------------- configuration.yaml schritt "configuration.yaml ergänzen" KONFIG="$ZIEL/configuration.yaml" SICHERUNG="$KONFIG.vor-datametric360-$(date +%Y%m%d_%H%M%S)" cp "$KONFIG" "$SICHERUNG" grau " Sicherung: $(basename "$SICHERUNG")" python3 - "$KONFIG" "$MARKE_START" "$MARKE_ENDE" <<'PYENDE' import re, sys pfad, start, ende = sys.argv[1], sys.argv[2], sys.argv[3] with open(pfad, encoding="utf-8") as f: inhalt = f.read() block = f"""{start} # Von install.sh eingefügt. Aenderungen innerhalb dieses Abschnitts werden bei # der naechsten Installation ersetzt - Eigenes bitte ausserhalb notieren. pyscript: allow_all_imports: true panel_custom: - name: audi-dashboard-panel sidebar_title: Mein Audi sidebar_icon: mdi:car-sports module_url: /local/audi-dashboard-panel.js?v=1 embed_iframe: false trust_external_script: false {ende}""" muster = re.compile(re.escape(start) + r".*?" + re.escape(ende), re.S) if muster.search(inhalt): inhalt = muster.sub(block, inhalt) print(" Abschnitt ersetzt") else: for schluessel in ("pyscript:", "panel_custom:"): if re.search(rf"^{schluessel}", inhalt, re.M): print(f" ACHTUNG: '{schluessel}' steht bereits ausserhalb des verwalteten Abschnitts.") print(" Der Abschnitt wird trotzdem angehaengt - Home Assistant meldet dann") print(" einen doppelten Schluessel. Bitte den alten Eintrag von Hand entfernen.") if not inhalt.endswith("\n"): inhalt += "\n" inhalt += "\n" + block + "\n" print(" Abschnitt angehaengt") with open(pfad, "w", encoding="utf-8") as f: f.write(inhalt) PYENDE # ------------------------------------------------------------------- Token schritt "Zugriffstoken für die Kilometerstand-Historie" TOKENDATEI="$ZIEL/audi_dashboard/ha_token.txt" if [ -n "$TOKEN" ]; then printf '%s' "$TOKEN" > "$TOKENDATEI" chmod 600 "$TOKENDATEI" 2>/dev/null || true gruen " Token gespeichert" elif [ -s "$TOKENDATEI" ]; then grau " Vorhandenes Token bleibt unangetastet" elif [ "$INTERAKTIV" = "ja" ] && [ -t 0 ]; then echo " Der Fahrtabschluss liest den Kilometerstand aus dem Verlauf und braucht dafür" echo " ein langlebiges Zugriffstoken (Profil → Sicherheit → Langlebige Zugriffstoken)." echo " Leer lassen und später nachtragen ist möglich - Fahrten bleiben dann ohne Strecke." printf " Token (Eingabe bleibt verborgen): " read -rs EINGABE || EINGABE="" echo if [ -n "$EINGABE" ]; then printf '%s' "$EINGABE" > "$TOKENDATEI" chmod 600 "$TOKENDATEI" 2>/dev/null || true gruen " Token gespeichert" else rot " Übersprungen - später in $TOKENDATEI ablegen." fi else rot " Kein Token angegeben. Später in $TOKENDATEI ablegen," rot " sonst bleiben Fahrten ohne Streckenangabe." fi # ------------------------------------------------------------------ Abschluss schritt "Fertig" cat <