Files
audi-app/homeassistant/installationspaket/install.ps1
T
tobias 5ff33c92d9 install.ps1: Zugangsdaten direkt nach der Pfadeingabe abfragen
Bisher wurde nach dem manuell eingegebenen config-Pfad erst ein
Erreichbarkeits-Test versucht (der ohne bestehende SMB-Sitzung fast immer
scheitert) und erst danach nach Benutzername/Kennwort gefragt. Jetzt folgt
die Abfrage direkt auf die Pfadeingabe. Die Anmeldelogik selbst ist dafür
in die Funktion SambaAnmelden ausgelagert, die weiterhin auch als
Rückfallebene für automatisch gefundene oder per -Ziel übergebene Pfade
dient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:43:15 +02:00

295 lines
15 KiB
PowerShell

<#
.SYNOPSIS
Installiert "Mein Audi" (DataMetric360) als Home-Assistant-Integration -
der Weg ohne HACS.
.BESCHREIBUNG
Kopiert genau einen Ordner:
custom_components\audi_dashboard\ -> <Ziel>\custom_components\
Mehr ist nicht nötig. Alles, was früher an dieser Stelle von Hand gemacht
werden musste, macht die Integration inzwischen selbst: sie meldet ihr Panel
an, liefert ihre Oberflächen-Dateien selbst aus, legt beim ersten Start ein
Fahrzeugprofil aus der Vorlage an und bringt pypdf über ihre manifest.json
mit, das Home Assistant beim ersten Laden nachinstalliert.
WOFÜR ES DIESES SKRIPT ÜBERHAUPT GIBT
--------------------------------------
HACS scheidet für dieses Repository aus, und zwar endgültig: laut eigener
Dokumentation (hacs.xyz/docs/faq/private_repositories) kann HACS
"grundsätzlich nicht" mit privaten GitHub-Repositories arbeiten - ohne
Ausnahme für Tokens oder verbundene Konten. Das Repository enthält die
Audi-Hausschrift und die Typenschilder, die nur für diese eine private
Installation lizenziert sind, und öffentlich machen ist deshalb keine
Option. Ein GitHub-Spiegel wurde deshalb 2026-08-24 wieder aufgegeben -
entwickelt wird ausschließlich auf der eigenen Gitea-Instanz.
Dieses Skript ist damit nicht die Rückfallebene, sondern der einzige Weg.
SICHERHEIT GEGENÜBER DER BESTEHENDEN HA-INSTALLATION
----------------------------------------------------
Die riskanteste Stelle der früheren Fassung gibt es nicht mehr: sie musste
die configuration.yaml ergänzen (pyscript:, panel_custom:), und eine kaputte
configuration.yaml ist der eine Weg, auf dem ein Installer Home Assistant am
Starten hindern kann. Diese Fassung fasst die Datei überhaupt nicht mehr an.
Was bleibt, ist nachprüfbar:
- Geschrieben wird ausschließlich in <Ziel>\custom_components\audi_dashboard.
Kein anderer Pfad wird angefasst - nicht .storage\, nicht
configuration.yaml, nicht automations.yaml, nicht www\, nicht
audi_dashboard\ (die Fahrzeugdaten).
- Entfernt wird nur der eigene Ordner, und nur, wenn darin eine
manifest.json mit "domain": "audi_dashboard" liegt. Ein fremder Ordner
unter diesem Namen führt zum Abbruch, nicht zum Löschen.
- Es startet Home Assistant nicht neu und greift nicht in den laufenden
Betrieb ein.
- -Pruefen zeigt den kompletten Ablauf, ohne irgendetwas zu schreiben.
Im Zweifel damit anfangen.
Warum der Zielordner vor dem Kopieren geleert wird (und nicht nur
überschrieben): bleibt eine Datei aus einer älteren Fassung liegen, die es
im neuen Stand nicht mehr gibt, lädt Home Assistant sie trotzdem mit. Das
ist genau der Fehler, den man hinterher am schwersten findet. HACS macht es
aus demselben Grund so.
Die Fahrzeugdaten liegen woanders (<Ziel>\audi_dashboard\) und werden von
diesem Skript nie berührt - ein Update kann sie deshalb gar nicht treffen.
.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 Pruefen
Nur anzeigen, was passieren würde - es wird nichts geschrieben.
.BEISPIEL
.\install.ps1
.\install.ps1 -Ziel "\\192.168.1.50\config"
.\install.ps1 -Pruefen
#>
param(
[string]$Ziel,
[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 }
# Test-Path fragt nie nach einer Anmeldung: ohne eine bereits bestehende
# SMB-Sitzung zu diesem Server liefert es bei einem passwortgeschützten Share
# einfach "false" - ganz ohne Fehlermeldung oder Passwortabfrage. Diese
# Funktion baut die Sitzung selbst auf, statt das als manuellen Vorschritt
# vorauszusetzen.
function SambaAnmelden($pfad) {
$nutzer = Read-Host " Benutzername [homeassistant]"
if (-not $nutzer) { $nutzer = "homeassistant" }
$kennwortSicher = Read-Host " Kennwort" -AsSecureString
$kennwort = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($kennwortSicher))
# Eine tote Verbindung von einem vorherigen Versuch würde den Neuaufbau
# blockieren - leise entfernen, falls vorhanden. "Nichts zu löschen" ist
# dabei der Normalfall, meldet sich aber als Fehler - mit
# $ErrorActionPreference "Stop" würde das sonst das ganze Skript
# abbrechen, deshalb hier abgefangen statt nur umgeleitet.
try { & net use $pfad /delete /y *> $null } catch { }
& net use $pfad /user:$nutzer $kennwort
$kennwort = $null
}
$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." }
# ------------------------------------------------------------ Quelle finden
# Zwei Lagen, damit sowohl der Aufruf aus dem Projektordner heraus als auch
# ein separat weitergegebenes Paket funktioniert.
$quellKandidaten = @(
(Join-Path $hier "custom_components\audi_dashboard"),
(Join-Path $hier "..\..\custom_components\audi_dashboard")
)
$quelle = $null
foreach ($k in $quellKandidaten) {
if (Test-Path (Join-Path $k "manifest.json")) { $quelle = (Resolve-Path $k).Path; break }
}
if (-not $quelle) {
throw "custom_components\audi_dashboard nicht gefunden - dieses Skript gehört in den Projektordner (oder das Paket daneben)."
}
$version = (Get-Content (Join-Path $quelle "manifest.json") -Raw | ConvertFrom-Json).version
Gut "Quelle: $quelle (Version $version)"
# ------------------------------------------------ OTA-Bündel auf Stand prüfen
# Das Bündel für die iOS-App (frontend\app\bundle.json, gebaut von
# companion-app\scripts\ota-paket.ps1) trägt seine eigene, zum Bauzeitpunkt
# eingefrorene Versionsnummer. Ändert sich manifest.json's Version, ohne dass
# jemand "npm run ota" erneut laufen lässt, bekommt die App zwei
# widersprüchliche Signale gleichzeitig: die Hinweisleiste sagt "du bist
# veraltet" (vergleicht gegen die live gelesene manifest-Version), die
# Update-Seite sagt "du bist aktuell" (vergleicht gegen das eingefrorene, nun
# falsche Bündel) - und es gibt keinen Weg, das Update zu bekommen, bis jemand
# den Widerspruch bemerkt. Das ist kein Zustand, den ein Installer schweigend
# durchwinken sollte - hier ist der eine Ort, an dem jede Auslieferung
# durchläuft, also der einzige verlässliche Punkt für diese Prüfung.
#
# Ein fehlendes Bündel ist dagegen kein Fehler: OTA ist eine optionale
# Zusatzfunktion, nicht jede Installation liefert eins mit.
$buendelInfo = Join-Path $quelle "frontend\app\bundle.json"
if (Test-Path $buendelInfo) {
$buendelVersion = (Get-Content $buendelInfo -Raw | ConvertFrom-Json).version
if ($buendelVersion -ne $version) {
Warnung "OTA-Bündel (frontend\app\bundle.json) steht auf Version $buendelVersion, die Integration auf $version."
Write-Host " Die App-Update-Funktion würde bis zur Behebung falsche Ergebnisse zeigen." -ForegroundColor Yellow
[void]$restliste.Add("OTA-Bündel ist veraltet (Version $buendelVersion statt $version): in companion-app\ 'npm run ota' ausführen, dann dieses Skript erneut laufen lassen.")
} else {
Info "OTA-Bündel passt zur Integration (Version $buendelVersion)"
}
}
# ---------------------------------------------------------------- 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." }
# Direkt nach der Pfadeingabe fragen, nicht erst nach einem
# (ohne bestehende Sitzung fast immer erfolglosen) Erreichbarkeits-
# Test - siehe SambaAnmelden oben.
if ($Ziel -match '^\\\\') {
Write-Host ""
SambaAnmelden $Ziel
}
}
}
if (-not (Test-Path $Ziel)) {
# Automatisch gefundene oder per -Ziel übergebene Pfade durchlaufen
# SambaAnmelden oben nicht - hier derselbe Weg als Rückfallebene, falls
# auch für sie eine Anmeldung fehlt.
if ($Ziel -match '^\\\\') {
Warnung "Kein Zugriff auf $Ziel - vermutlich fehlt die Anmeldung am Samba-Share."
Write-Host ""
SambaAnmelden $Ziel
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $Ziel)) {
throw "Ziel nicht erreichbar: $Ziel - Anmeldung fehlgeschlagen oder Pfad falsch."
}
Gut "Angemeldet."
} else {
throw "Ziel nicht erreichbar: $Ziel - Pfad korrekt?"
}
}
# Ein config-Verzeichnis ohne configuration.yaml ist mit hoher Wahrscheinlichkeit
# der falsche Ordner - lieber hier abbrechen als Dateien irgendwohin streuen.
if (-not (Test-Path (Join-Path $Ziel "configuration.yaml"))) {
throw "In $Ziel liegt keine configuration.yaml - ist das wirklich das config-Verzeichnis von HA?"
}
Gut "Ziel: $Ziel"
# ------------------------------------------------------- Zielordner prüfen
$zielOrdner = Join-Path $Ziel "custom_components\audi_dashboard"
$vorhandeneVersion = $null
if (Test-Path $zielOrdner) {
$zielManifest = Join-Path $zielOrdner "manifest.json"
if (-not (Test-Path $zielManifest)) {
throw "In $zielOrdner liegt keine manifest.json. Das ist nicht unser Ordner - Abbruch, es wird nichts angefasst."
}
$vorhandenesManifest = Get-Content $zielManifest -Raw | ConvertFrom-Json
if ($vorhandenesManifest.domain -ne "audi_dashboard") {
throw "$zielOrdner gehört zur Integration '$($vorhandenesManifest.domain)', nicht zu 'audi_dashboard' - Abbruch."
}
$vorhandeneVersion = $vorhandenesManifest.version
Info "bereits installiert: Version $vorhandeneVersion"
}
# ----------------------------------------------------------------- Kopieren
Schritt "Integration kopieren"
if ($Pruefen) {
if ($vorhandeneVersion) { Info "würde ersetzen: $zielOrdner (Version $vorhandeneVersion -> $version)" }
else { Info "würde anlegen: $zielOrdner (Version $version)" }
Info "würde sonst nichts anfassen - keine configuration.yaml, keine Fahrzeugdaten"
} else {
New-Item -ItemType Directory -Force -Path (Join-Path $Ziel "custom_components") | Out-Null
if (Test-Path $zielOrdner) {
# Die Prüfung oben hat bestätigt, dass das unser Ordner ist.
Remove-Item $zielOrdner -Recurse -Force
}
Copy-Item $quelle $zielOrdner -Recurse -Force
# Übersetzte Reste einer früheren Fassung gehören nicht ins Ziel.
Get-ChildItem $zielOrdner -Recurse -Directory -Filter "__pycache__" |
ForEach-Object { Remove-Item $_.FullName -Recurse -Force }
# Rücklesen: liegt im Ziel wirklich das, was gemeint war?
$kontrolle = Join-Path $zielOrdner "manifest.json"
if (-not (Test-Path $kontrolle)) {
throw "Nach dem Kopieren fehlt $kontrolle - die Installation ist unvollständig."
}
$geschrieben = (Get-Content $kontrolle -Raw | ConvertFrom-Json).version
if ($geschrieben -ne $version) {
throw "Nach dem Kopieren steht Version '$geschrieben' statt '$version' im Ziel - die Installation ist unvollständig."
}
$anzahl = (Get-ChildItem $zielOrdner -Recurse -File).Count
Gut "custom_components\audi_dashboard (Version $version, $anzahl Dateien)"
}
# --------------------------------------------------------------- Restliste
if (-not $vorhandeneVersion) {
[void]$restliste.Add("Home Assistant neu starten: Einstellungen -> System -> Neu starten. (Beim ersten Start ladt HA die Abhaengigkeit pypdf nach - das kann eine Minute dauern.)")
[void]$restliste.Add("Integration hinzufuegen: Einstellungen -> Geraete & Dienste -> Integration hinzufuegen -> 'Audi Dashboard'. Danach steht 'Mein Audi' in der Seitenleiste.")
[void]$restliste.Add("In der App: Einstellungen -> Fahrzeug einrichten -> Setup - Sensoren zuordnen. Im Auslieferstand ist KEIN Sensor vorbelegt; zwingend fuer die Fahrterkennung ist der Zuendungs-/ACC-Sensor (binary_sensor, meist vom Teltonika FMM003). Ein Neustart ist dafuer nicht mehr noetig - Aenderungen wirken sofort.")
} else {
[void]$restliste.Add("Home Assistant neu starten: Einstellungen -> System -> Neu starten. (Oder kuerzer: Einstellungen -> Geraete & Dienste -> Audi Dashboard -> Neu laden.)")
}
# Bewusst nicht automatisch eingefügt: der recorder-Block ist eine Entscheidung
# über den Plattenplatz der Instanz (ein Jahr Fahrzeugverlauf sind grob 1-1,5 GB,
# siehe Kopf von recorder_snippet.yaml), und viele Instanzen haben bereits einen
# eigenen recorder:-Block mit anderen Einstellungen. Zusammenführen ist Handarbeit.
[void]$restliste.Add("ZEITKRITISCH - Datenaufbewahrung: den Block aus recorder_snippet.yaml in die configuration.yaml uebernehmen. Home Assistant loescht Sensor-Verlaeufe sonst nach 10 Tagen; was weg ist, laesst sich auch mit 'Daten importieren aus Home Assistant' nicht mehr nachtragen. Je frueher der Block drin ist, desto mehr Vergangenheit bleibt erhalten. VORHER Einstellungen -> System -> Speicher pruefen: ein Jahr Verlauf braucht grob 1-1,5 GB. Bei wenig freiem Platz (SD-Karte, kleine eMMC) mit purge_keep_days: 90 anfangen - Details im Kopf der Datei.")
[void]$restliste.Add("Optional, sobald die Sensoren zugeordnet sind: Einstellungen -> Einrichten -> 'Daten importieren aus Home Assistant' holt Fahrten, Tankvorgaenge und Spannungswerte aus dem bereits aufgezeichneten HA-Verlauf nach.")
Write-Host ""
if ($Pruefen) {
Write-Host " Pruefmodus beendet - es wurde nichts geschrieben." -ForegroundColor Yellow
} else {
Write-Host " Die Integration ist installiert." -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 " Fuer spaetere Updates reicht derselbe Aufruf noch einmal." -ForegroundColor White
Write-Host " Die Fahrzeugdaten unter audi_dashboard\ bleiben dabei unangetastet."
Write-Host ""