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:
@@ -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 1–3 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 ""
|
||||
Reference in New Issue
Block a user