484f2ffe6d
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>
282 lines
12 KiB
PowerShell
282 lines
12 KiB
PowerShell
<#
|
|
.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 ""
|