diff --git a/homeassistant/installationspaket/ANLEITUNG.md b/homeassistant/installationspaket/ANLEITUNG.md index 0e32208..94e178d 100644 --- a/homeassistant/installationspaket/ANLEITUNG.md +++ b/homeassistant/installationspaket/ANLEITUNG.md @@ -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 `\\\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 diff --git a/homeassistant/installationspaket/Installieren.cmd b/homeassistant/installationspaket/Installieren.cmd new file mode 100644 index 0000000..3eebd5b --- /dev/null +++ b/homeassistant/installationspaket/Installieren.cmd @@ -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% diff --git a/homeassistant/installationspaket/install.ps1 b/homeassistant/installationspaket/install.ps1 new file mode 100644 index 0000000..690a187 --- /dev/null +++ b/homeassistant/installationspaket/install.ps1 @@ -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\ -> \pyscript\ + 2. data\ -> \audi_dashboard\ (Ordner wird umbenannt) + fahrzeugprofil.example.json -> fahrzeugprofil.json + 3. www\ -> \www\ + 4. configuration_snippet.yaml wird in \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 ""