Installationsskript: eine Zeile statt dreissig Minuten Handarbeit

homeassistant/install.sh richtet das Panel samt Backend auf einer
Home-Assistant-Instanz ein und verdrahtet dabei die Selbstaktualisierung, so
dass danach alles Weitere ueber die Weboberflaeche laeuft: Sensoren zuordnen,
nach Updates suchen, einspielen.

Gedacht fuer das Add-on "Terminal & SSH" direkt auf der Instanz:
  bash <(curl -fsSL <ROH-URL>/homeassistant/install.sh)
Alternativ von einem Rechner aus mit --ziel gegen ein eingebundenes
config-Verzeichnis, oder mit --von gegen eine lokale Arbeitskopie.

Was es tut: pyscript installieren (neueste Version von GitHub, falls nicht
vorhanden), pyscript/ und www/ einspielen, den Belegleser mitliefern (er
liegt in data/, ist aber Code), fehlende Datenbestaende aus der Vorlage
anlegen, den Abschnitt in die configuration.yaml eintragen und
UPDATE_REPO_URL setzen.

Drei Eigenschaften, auf die es dabei ankommt:

- Mehrfach ausfuehrbar. Der Abschnitt in der configuration.yaml steht
  zwischen Markern und wird beim zweiten Lauf ersetzt statt angehaengt.
  Stehen pyscript oder panel_custom bereits ausserhalb dieses Abschnitts,
  weist das Skript darauf hin, statt einen doppelten Schluessel zu erzeugen.
- Vorhandene Daten bleiben unangetastet. Fahrzeugprofil, Fahrten,
  Tankvorgaenge und ein bereits hinterlegtes Token werden nie ueberschrieben,
  nur fehlende Dateien angelegt.
- Die configuration.yaml wird vor jeder Aenderung mit Zeitstempel gesichert.

Geprueft gegen eine frische Home-Assistant-Instanz im Container: HA startet
ohne Konfigurationsfehler, alle neun pyscript-Entitaeten werden
veroeffentlicht, das Panel ist als "Mein Audi" mit mdi:car-sports unter
/audi-dashboard-panel registriert und rendert im Browser mit fuenf Tabs. Ein
zweiter Durchlauf laesst Profil und Token unveraendert und den
Konfigurationsabschnitt genau einmal stehen.

Ausserdem den blockierenden Verlaufsabruf in fahrtabschluss_logik.py
uebernommen: urlopen lief direkt statt ueber task.executor, was Home
Assistant seit 2026.8 abbricht - jede Fahrt waere ohne Strecke geblieben.
Der Fix lag bisher nur im Branch umsetzung-datametric360; eine frische
Installation haette den Fehler sonst mitgebracht.

INSTALL.md nennt den schnellen Weg jetzt vor der ausfuehrlichen Anleitung.
This commit is contained in:
Paul Nothaft
2026-08-13 16:20:34 +02:00
parent 78d70fa4ca
commit c11c492d86
3 changed files with 348 additions and 7 deletions
+39
View File
@@ -34,6 +34,45 @@ Zeitaufwand: ca. 3040 Minuten, größtenteils Warten auf Neustarts.
---
## Der schnelle Weg: install.sh
Für eine neue Instanz gibt es ein Skript, das die Schritte 1 bis 3 und 5
zusammen erledigt. Am einfachsten direkt auf der HA-Instanz im Add-on
**Terminal & SSH**:
```bash
bash <(curl -fsSL https://gitea.nothaft.cloud/paul/audi-app/raw/branch/main/homeassistant/install.sh)
```
Liegt das Repository privat (Standard), braucht der Aufruf ein Zugangstoken:
```bash
curl -fsSL -H "Authorization: token <DEIN-GITEA-TOKEN>" \
https://gitea.nothaft.cloud/paul/audi-app/raw/branch/main/homeassistant/install.sh -o install.sh
bash install.sh --repo https://<nutzer>:<token>@gitea.nothaft.cloud/paul/audi-app.git
```
Alternativ von einem Rechner aus gegen ein eingebundenes config-Verzeichnis:
```bash
./install.sh --ziel /Volumes/config --von ~/Development/audi-app
```
Das Skript installiert pyscript, spielt Backend und Oberfläche ein, legt das
Fahrzeugprofil aus der Vorlage an (**vorhandene Daten bleiben unangetastet**),
trägt den Abschnitt in die `configuration.yaml` ein — mit Sicherungskopie und
so, dass ein zweiter Lauf ihn ersetzt statt anhängt — und verdrahtet die
Selbstaktualisierung, damit „Update suchen" in der App funktioniert.
`--hilfe` zeigt alle Optionen.
Danach bleiben nur noch drei Dinge, alle in der Weboberfläche: neu starten,
Sensoren zuordnen (Schritt 4 unten), Fahrzeugdaten eintragen.
Wer lieber jeden Schritt selbst nachvollzieht, folgt der ausführlichen
Anleitung ab hier.
## Schritt 1 — pyscript installieren
1. In Home Assistant: **HACS → Integrationen → Explore & Download Repositories**
+293
View File
@@ -0,0 +1,293 @@
#!/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 <ROH-URL dieser Datei>)
# 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"
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 <pfad> config-Verzeichnis von Home Assistant (Standard: /config)
--von <pfad> lokale Arbeitskopie benutzen statt zu klonen
--repo <url> Repository zum Klonen und für die Selbstaktualisierung
--branch <name> Branch (Standard: main)
--token <wert> 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
ENDE
}
while [ $# -gt 0 ]; do
case "$1" in
--ziel) ZIEL="${2:-}"; shift 2 ;;
--von) QUELLE="${2:-}"; shift 2 ;;
--repo) 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 <pfad> 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://<nutzer>:<token>@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"
# UPDATE_REPO_URL im ausgelieferten einstellungen.py setzen, damit
# "Update suchen" in den Einstellungen der App funktioniert.
python3 - "$ZIEL/pyscript/modules/einstellungen.py" "$REPO" "$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
grau " Updatequelle: $REPO ($BRANCH)"
grau " Danach in der App unter Einstellungen → Version → \"Update suchen\""
# ---------------------------------------------------------- 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 <<ENDE
Noch zu tun, alles in der Weboberfläche:
1. Home Assistant neu starten
Einstellungen → System → oben rechts → Neu starten
2. Sensoren zuordnen
Mein Audi → Einstellungen → Fahrzeug einrichten → Einrichten
→ "Setup — Sensoren zuordnen"
Pflicht ist nur die Zündung; alles Weitere ist optional.
3. Fahrzeugdaten eintragen
Im selben Menü unter "Einrichten": Kennzeichen, FIN, Versicherung, …
Ab dann läuft alles über die Oberfläche - auch Updates:
Mein Audi → Einstellungen → Version → "Update suchen"
Sicherung der configuration.yaml:
$SICHERUNG
ENDE
gruen "Installation abgeschlossen."
@@ -51,10 +51,19 @@ def _token_lesen():
def historie_abfragen(entity_id, start_iso, end_iso):
"""urllib.request.urlopen läuft direkt, nicht über task.executor - anders
als ein bloßes open() ist das ein modulqualifizierter Aufruf einer echten
externen Funktion, der an der Testinstanz ohne Fehler durchlief. Blockiert
kurz, läuft nur beim Fahrtabschluss-Screening, nicht bei jedem Tick."""
"""Fragt die Verlaufsdaten des Kilometerstand-Sensors über die REST-API ab.
Beide blockierenden Schritte laufen über task.executor. Ein früherer Stand
rief urlopen() direkt auf - das lief jahrelang durch, bis Home Assistant
anfing, blockierende Aufrufe im Ereignisschleifen-Thread zu erkennen und
abzubrechen (real beobachtet ab 2026.8: "Caught blocking call to
putrequest ... inside the event loop"). Seitdem schlug jedes Screening
fehl, und manuell angelegte Fahrten blieben ohne Strecke - sichtbar nur
als Warnung im Protokoll, weil der Aufrufer den Fehler bewusst abfängt.
task.executor braucht echte externe Funktionen; urllib.request.urlopen und
die read-Methode der Antwort sind genau das (dieselbe Einschränkung wie bei
io.open in profil.py)."""
token = _token_lesen()
start_enc = urllib.parse.quote(start_iso)
end_enc = urllib.parse.quote(end_iso)
@@ -63,10 +72,10 @@ def historie_abfragen(entity_id, start_iso, end_iso):
f"?filter_entity_id={entity_id}&end_time={end_enc}"
)
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
resp = urllib.request.urlopen(req, timeout=10)
rohtext = resp.read().decode("utf-8")
resp = task.executor(urllib.request.urlopen, req, timeout=10)
rohdaten = task.executor(resp.read)
resp.close()
daten = json.loads(rohtext)
daten = json.loads(rohdaten.decode("utf-8"))
return daten[0] if daten else []