Add one-click installer to installationspaket

Installieren.cmd (double-click) wraps install.ps1, which automates steps 1-3
of ANLEITUNG.md: find the HA config share, copy pyscript/, data/ ->
audi_dashboard/ and www/, merge the configuration.yaml snippet, optionally
write ha_token.txt. The .cmd wrapper exists because double-clicking a .ps1
opens it in an editor and the execution policy blocks it; the bypass applies
to that one call only.

Designed to be safe to re-run:
- Never overwrites fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl,
  entitaeten.json or ha_token.txt, so re-running on a live install keeps the
  vehicle profile, trips, fills and sensor mapping.
- configuration.yaml is backed up to .bak first; the inserted part sits
  between marker lines and gets replaced instead of appended twice.
- If pyscript: or panel_custom: are already there from another source, the
  file is left untouched and the script prints what to merge by hand -
  duplicate top-level keys would be invalid YAML and HA would not start.
- Aborts before writing if the target has no configuration.yaml.

Steps that cannot be automated from a Windows share (HA restart, pip install
pypdf, sensor mapping) are collected into a closing to-do list. -Pruefen shows
what would happen without writing.

install.ps1 is stored UTF-8 *with* BOM - PowerShell 5.1 reads scripts as ANSI
otherwise and mangles the umlauts; the files it writes stay BOM-less.

Verified against a fake config tree: fresh install, re-run idempotency,
existing-data preservation, foreign-key refusal, wrong-directory abort, and
the merged configuration.yaml parses as valid YAML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 13:02:02 +02:00
parent fb38c297bd
commit 484f2ffe6d
3 changed files with 353 additions and 0 deletions
@@ -8,6 +8,52 @@ nur die Checkliste.
- HACS installiert, darüber **pyscript** installiert (HACS → Integrationen →
„pyscript" suchen → Download → HA neu starten)
- Das **Samba-Share-Add-on** in HA läuft, damit `\\<HA-IP>\config` vom PC aus
erreichbar ist
---
## Der schnelle Weg: `Installieren.cmd`
**Doppelklick auf `Installieren.cmd`** — das erledigt die Schritte 13 unten von
selbst: es sucht die HA-Instanz im Netz (`\\homeassistant\config` und die
üblichen Alternativen, sonst fragt es nach dem Pfad), kopiert `pyscript\`,
`data\``audi_dashboard\` und `www\`, trägt die beiden Blöcke in die
`configuration.yaml` ein und listet am Ende auf, was noch von Hand zu tun ist
(Neustart, Token, Sensoren zuordnen).
Was es dabei **nicht** tut:
- Bestehende Daten überschreiben. `fahrzeugprofil.json`, `fahrten.jsonl`,
`tankvorgaenge.jsonl`, `entitaeten.json` und `ha_token.txt` bleiben
unangetastet — ein zweiter Lauf auf einer laufenden Installation ist deshalb
gefahrlos.
- Die `configuration.yaml` blind verändern. Sie wird vorher nach
`configuration.yaml.bak` gesichert, und stehen dort schon eigene
`pyscript:`- oder `panel_custom:`-Einträge, fasst das Skript sie gar nicht an
und sagt stattdessen, was zu ergänzen ist (zwei gleiche Top-Level-Schlüssel
wären ungültiges YAML — HA würde nicht mehr starten).
Erst schauen, was passieren würde, ohne etwas zu schreiben:
```powershell
.\install.ps1 -Pruefen
```
Mit festem Ziel und Token in einem Rutsch:
```powershell
.\install.ps1 -Ziel "\\192.168.1.50\config" -Token "eyJhb..."
```
Danach weiter bei **Schritt 3** (Token, falls nicht übergeben), **4** und **5**.
---
## Der Weg von Hand
Falls `Installieren.cmd` nicht durchläuft oder lieber nachvollziehbar
Schritt für Schritt gearbeitet werden soll:
## 1. Dateien kopieren
@@ -0,0 +1,26 @@
@echo off
REM ---------------------------------------------------------------------------
REM Mein Audi (DataMetric360) - Installation per Doppelklick.
REM
REM Existiert, weil ein Doppelklick auf eine .ps1 sie NICHT ausfuehrt, sondern
REM im Editor oeffnet - und die Ausfuehrungsrichtlinie sie zusaetzlich blockt.
REM Dieser Wrapper startet install.ps1 mit -ExecutionPolicy Bypass, gilt aber
REM nur fuer diesen einen Aufruf: die Einstellung des Systems bleibt, wie sie
REM ist. Deshalb hier -File und kein Herunterschrauben per Set-ExecutionPolicy.
REM ---------------------------------------------------------------------------
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" %*
set FEHLER=%ERRORLEVEL%
echo.
if not "%FEHLER%"=="0" (
echo Die Installation wurde mit einem Fehler abgebrochen ^(Code %FEHLER%^).
echo Die Meldung darueber sagt, woran es lag.
echo.
)
REM Fenster offen halten - sonst ist die Ausgabe beim Doppelklick sofort weg.
pause
endlocal
exit /b %FEHLER%
@@ -0,0 +1,281 @@
<#
.SYNOPSIS
Installiert "Mein Audi" (DataMetric360) in einem Rutsch auf einer neuen
Home-Assistant-Instanz - die Schritte 1-3 aus ANLEITUNG.md.
.BESCHREIBUNG
Nimmt dem Erstaufsetzen die Handarbeit ab:
1. pyscript\ -> <Ziel>\pyscript\
2. data\ -> <Ziel>\audi_dashboard\ (Ordner wird umbenannt)
fahrzeugprofil.example.json -> fahrzeugprofil.json
3. www\ -> <Ziel>\www\
4. configuration_snippet.yaml wird in <Ziel>\configuration.yaml eingefügt
5. ha_token.txt wird geschrieben, wenn -Token übergeben wurde
Grundsatz: nichts kaputtmachen, was schon da ist. Das Skript ist darauf
ausgelegt, gefahrlos mehrfach zu laufen (etwa nach einem Abbruch):
- Bestandsdaten (fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl,
entitaeten.json, ha_token.txt) werden NIE überschrieben. Wer eine
laufende Installation neu bespielt, behält sein Fahrzeugprofil, seine
Fahrten, Tankvorgänge und Sensorzuordnung.
- configuration.yaml wird vor jeder Änderung nach configuration.yaml.bak
kopiert. Der eingefügte Teil steht zwischen zwei Markierungszeilen und
wird bei einem erneuten Lauf ersetzt statt ein zweites Mal angehängt.
- Stehen `pyscript:` oder `panel_custom:` dort schon aus anderer Quelle,
fasst das Skript die Datei NICHT an, sondern zeigt an, was von Hand zu
ergänzen ist. Zwei gleiche Top-Level-Schlüssel wären ungültiges YAML und
HA würde nicht mehr starten - das ist keine Entscheidung, die ein
Installer still für jemanden treffen sollte.
Nicht automatisierbar und deshalb am Ende als Restliste ausgegeben:
HA-Neustart, `pip install pypdf` (braucht eine Shell auf der HA-Maschine)
und das Zuordnen der Sensoren im Setup-Menü der App.
.PARAMETER Ziel
config-Verzeichnis der HA-Instanz, z. B. "\\homeassistant.local\config"
(Samba-Add-on) oder ein lokal gemounteter Pfad. Ohne Angabe sucht das
Skript die üblichen Samba-Namen selbst ab.
.PARAMETER Token
Long-Lived Access Token aus HA (Profil-Avatar -> "Long-lived access
tokens"). Wird nur nach audi_dashboard\ha_token.txt geschrieben und für das
Kilometerstand-Screening gebraucht. Ohne Angabe wird die Datei nicht
angelegt und der Schritt landet in der Restliste. Eine schon vorhandene
Datei bleibt in jedem Fall unangetastet.
.PARAMETER Pruefen
Nur anzeigen, was passieren würde - es wird nichts geschrieben.
.BEISPIEL
.\install.ps1
.\install.ps1 -Ziel "\\192.168.1.50\config"
.\install.ps1 -Ziel "\\homeassistant.local\config" -Token "eyJhb..."
.\install.ps1 -Pruefen
#>
param(
[string]$Ziel,
[string]$Token,
[switch]$Pruefen
)
$ErrorActionPreference = "Stop"
$hier = $PSScriptRoot
function Schritt($text) { Write-Host "`n$text" -ForegroundColor Cyan }
function Gut($text) { Write-Host " [ok] $text" -ForegroundColor Green }
function Info($text) { Write-Host " [info] $text" -ForegroundColor DarkGray }
function Warnung($text) { Write-Host " [!] $text" -ForegroundColor Yellow }
# Sammelt alles ein, was der Mensch am Ende noch selbst tun muss.
$restliste = New-Object System.Collections.ArrayList
Write-Host ""
Write-Host " Mein Audi - Installation" -ForegroundColor White
Write-Host " ========================" -ForegroundColor White
if ($Pruefen) { Warnung "Pruefmodus - es wird nichts geschrieben." }
# ---------------------------------------------------------------- Ziel finden
if (-not $Ziel) {
Schritt "Suche Home Assistant ..."
# Die Reihenfolge ist Absicht: der kurze Name antwortet in den meisten
# Heimnetzen schneller als der .local-Name über mDNS.
$kandidaten = @(
"\\homeassistant\config",
"\\homeassistant.local\config",
"\\hassio\config"
)
foreach ($k in $kandidaten) {
Info "probiere $k"
if (Test-Path $k) { $Ziel = $k; break }
}
if (-not $Ziel) {
Write-Host ""
Warnung "Keine HA-Instanz automatisch gefunden."
Write-Host " Bitte den config-Pfad angeben, z. B. \\192.168.1.50\config"
Write-Host " (Samba-Share-Add-on in HA muss laufen und verbunden sein.)"
Write-Host ""
$Ziel = Read-Host " config-Pfad"
if (-not $Ziel) { throw "Kein Ziel angegeben - Abbruch." }
}
}
if (-not (Test-Path $Ziel)) {
throw "Ziel nicht erreichbar: $Ziel - Samba-Share verbunden? Pfad korrekt?"
}
Gut "Ziel: $Ziel"
# Ein config-Verzeichnis ohne configuration.yaml ist mit hoher Wahrscheinlichkeit
# der falsche Ordner - lieber hier abbrechen als Dateien irgendwohin streuen.
$configYaml = Join-Path $Ziel "configuration.yaml"
if (-not (Test-Path $configYaml)) {
throw "In $Ziel liegt keine configuration.yaml - ist das wirklich das config-Verzeichnis von HA?"
}
# pyscript wird über HACS installiert, nicht von hier - ohne die Integration
# läuft das komplette Backend nicht, deshalb früh darauf hinweisen.
if (-not (Test-Path (Join-Path $Ziel "custom_components\pyscript"))) {
Warnung "custom_components\pyscript fehlt - pyscript ist offenbar noch nicht über HACS installiert."
[void]$restliste.Add("pyscript über HACS installieren (HACS -> Integrationen -> 'pyscript' -> Download), danach dieses Skript erneut laufen lassen.")
}
# --------------------------------------------------------------- 1. pyscript
Schritt "1/5 pyscript-Backend kopieren"
if ($Pruefen) {
Info "würde kopieren: $hier\pyscript\* -> $Ziel\pyscript\"
} else {
New-Item -ItemType Directory -Force -Path (Join-Path $Ziel "pyscript") | Out-Null
# Bewusst kein robocopy /MIR: das würde fremde Skripte im selben Ordner
# löschen. Hier wird nur ergänzt und überschrieben.
Copy-Item (Join-Path $hier "pyscript\*") (Join-Path $Ziel "pyscript") -Recurse -Force
Gut "pyscript\ kopiert"
}
# ------------------------------------------------------- 2. data -> audi_dashboard
Schritt "2/5 Datenverzeichnis anlegen (data -> audi_dashboard)"
$datenZiel = Join-Path $Ziel "audi_dashboard"
# Dateien, die einer laufenden Installation gehören - nie überschreiben.
$bestandsdaten = @(
"fahrzeugprofil.json",
"fahrten.jsonl",
"tankvorgaenge.jsonl",
"entitaeten.json",
"ha_token.txt"
)
if (-not $Pruefen) { New-Item -ItemType Directory -Force -Path $datenZiel | Out-Null }
foreach ($datei in Get-ChildItem (Join-Path $hier "data") -File) {
# fahrzeugprofil.example.json ist die Vorlage und heißt am Ziel anders.
$zielName = $datei.Name
if ($zielName -eq "fahrzeugprofil.example.json") { $zielName = "fahrzeugprofil.json" }
$zielPfad = Join-Path $datenZiel $zielName
if (($bestandsdaten -contains $zielName) -and (Test-Path $zielPfad)) {
Info "$zielName existiert bereits - bleibt unverändert"
continue
}
if ($Pruefen) {
Info "würde schreiben: $zielPfad"
} else {
Copy-Item $datei.FullName $zielPfad -Force
Gut "$zielName"
}
}
# --------------------------------------------------------------- 3. Frontend
Schritt "3/5 Frontend kopieren"
if ($Pruefen) {
Info "würde kopieren: $hier\www\* -> $Ziel\www\"
} else {
New-Item -ItemType Directory -Force -Path (Join-Path $Ziel "www") | Out-Null
Copy-Item (Join-Path $hier "www\*") (Join-Path $Ziel "www") -Recurse -Force
# Wie in update.ps1: die Versionsdatei bestimmt, ob Browser den neuen Code
# ziehen. Immer frisch schreiben, sonst zeigt ein Browser, der die App
# schon einmal geladen hatte, weiter den alten Stand.
$version = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$versionsDatei = Join-Path $Ziel "www\audi-dashboard-version.json"
[System.IO.File]::WriteAllText($versionsDatei, "{ ""version"": $version }", (New-Object System.Text.UTF8Encoding($false)))
Gut "www\ kopiert (Version $version)"
}
# ---------------------------------------------------- 4. configuration.yaml
Schritt "4/5 configuration.yaml ergänzen"
$markeAuf = "# >>> Mein Audi (DataMetric360) - automatisch eingefügt, nicht von Hand bearbeiten"
$markeZu = "# <<< Mein Audi (DataMetric360)"
# Beide Dateien ausdrücklich als UTF-8 lesen: Get-Content nimmt in
# PowerShell 5.1 sonst die ANSI-Codepage an und macht aus "Ergänzung" ein
# "Ergänzung", das dann so in der configuration.yaml landet.
$utf8OhneBom = New-Object System.Text.UTF8Encoding($false)
$snippet = [System.IO.File]::ReadAllText((Join-Path $hier "configuration_snippet.yaml"), $utf8OhneBom)
$inhalt = [System.IO.File]::ReadAllText($configYaml, $utf8OhneBom)
if ($null -eq $inhalt) { $inhalt = "" }
$block = "$markeAuf`r`n$snippet`r`n$markeZu`r`n"
$schonDa = $inhalt.Contains($markeAuf)
# Top-Level-Schlüssel aus fremder Quelle? Dann Finger weg (siehe Kopfkommentar).
$ohneUnserenBlock = $inhalt
if ($schonDa) {
$ohneUnserenBlock = [regex]::Replace(
$inhalt,
"(?ms)" + [regex]::Escape($markeAuf) + ".*?" + [regex]::Escape($markeZu) + "\r?\n?",
""
)
}
$fremdePyscript = [regex]::IsMatch($ohneUnserenBlock, "(?m)^pyscript:")
$fremdePanel = [regex]::IsMatch($ohneUnserenBlock, "(?m)^panel_custom:")
if ($fremdePyscript -or $fremdePanel) {
Warnung "configuration.yaml enthält bereits eigene Einträge - sie wird NICHT verändert."
if ($fremdePyscript) { Info "gefunden: pyscript:" }
if ($fremdePanel) { Info "gefunden: panel_custom:" }
Write-Host ""
Write-Host " Bitte von Hand zusammenführen (Inhalt aus configuration_snippet.yaml):" -ForegroundColor Yellow
Write-Host " - unter pyscript: allow_all_imports: true"
Write-Host " - unter panel_custom: den Listeneintrag 'audi-dashboard-panel'"
[void]$restliste.Add("configuration.yaml von Hand ergänzen (siehe Hinweis oben) - zwei gleiche Top-Level-Schlüssel wären ungültiges YAML.")
} elseif ($Pruefen) {
if ($schonDa) { Info "würde den vorhandenen Mein-Audi-Block ersetzen" }
else { Info "würde den Mein-Audi-Block anhängen" }
} else {
Copy-Item $configYaml "$configYaml.bak" -Force
Info "Sicherung: configuration.yaml.bak"
if ($schonDa) {
$neu = [regex]::Replace(
$inhalt,
"(?ms)" + [regex]::Escape($markeAuf) + ".*?" + [regex]::Escape($markeZu) + "\r?\n?",
[System.Text.RegularExpressions.MatchEvaluator]{ param($m) $block }
)
Gut "vorhandenen Block ersetzt"
} else {
$trenner = ""
if ($inhalt.Length -gt 0 -and -not $inhalt.EndsWith("`n")) { $trenner = "`r`n" }
$neu = $inhalt + $trenner + "`r`n" + $block
Gut "Block angehängt"
}
# Ohne BOM schreiben - HA liest die Datei als YAML, ein BOM hat da nichts
# zu suchen (Out-File -Encoding utf8 setzt in PowerShell 5.1 eines).
[System.IO.File]::WriteAllText($configYaml, $neu, (New-Object System.Text.UTF8Encoding($false)))
}
# --------------------------------------------------------------- 5. Token
Schritt "5/5 Zugriffstoken"
$tokenDatei = Join-Path $datenZiel "ha_token.txt"
if (Test-Path $tokenDatei) {
Info "ha_token.txt existiert bereits - bleibt unverändert"
} elseif ($Token) {
if ($Pruefen) {
Info "würde schreiben: $tokenDatei"
} else {
[System.IO.File]::WriteAllText($tokenDatei, $Token.Trim(), (New-Object System.Text.UTF8Encoding($false)))
Gut "ha_token.txt geschrieben"
}
} else {
Info "kein -Token übergeben"
[void]$restliste.Add("Long-Lived Access Token anlegen (HA: Profil-Avatar unten links -> 'Long-lived access tokens' -> Token erstellen) und den Wert in $tokenDatei speichern - nur den Token, keine Anführungszeichen. Wird fürs Kilometerstand-Screening gebraucht.")
}
# --------------------------------------------------------------- Restliste
[void]$restliste.Add("Home Assistant neu starten: Einstellungen -> System -> Neu starten.")
[void]$restliste.Add("Nur für Tankbeleg-Upload: auf der HA-Maschine 'pip install pypdf' ausführen (Terminal-&-SSH-Add-on oder docker exec).")
[void]$restliste.Add("Nach dem Neustart in der App: Einstellungen -> Fahrzeug einrichten -> Setup - Sensoren zuordnen. Zwingend für die Fahrterkennung ist der Zündungs-/ACC-Sensor (binary_sensor, meist vom Teltonika FMM003).")
Write-Host ""
if ($Pruefen) {
Write-Host " Pruefmodus beendet - es wurde nichts geschrieben." -ForegroundColor Yellow
} else {
Write-Host " Dateien sind kopiert." -ForegroundColor Green
}
Write-Host ""
Write-Host " Das fehlt noch:" -ForegroundColor White
$nr = 1
foreach ($punkt in $restliste) {
Write-Host " $nr. $punkt"
$nr++
}
Write-Host ""
Write-Host " Danach erscheint 'Mein Audi' in der Seitenleiste." -ForegroundColor White
Write-Host " Spätere Updates: update.ps1 statt install.ps1 - das lässt audi_dashboard\ in Ruhe."
Write-Host ""