Files
audi-app/companion-app/scripts/ota-paket.ps1
T
tobias 1354ca6b06 OTA-Updates für die iOS-App; HACS-Fehlannahme korrigiert
HACS kann laut eigener Dokumentation grundsätzlich nicht mit privaten
GitHub-Repositories arbeiten (hacs.xyz/docs/faq/private_repositories) - keine
Ausnahme für Tokens oder verbundene Konten. Meine frühere Annahme, HACS käme
damit zurecht, wenn es unter dem richtigen Konto angemeldet ist, war falsch.
Da das Repository aus Lizenzgründen privat bleiben muss (Audi-Hausschrift,
Typenschilder), ist install.ps1 damit nicht die Rückfallebene, sondern der
einzige Installationsweg - README, INSTALL.md, ANLEITUNG.md, install.ps1 und
VERSIONIERUNG.md korrigiert.

Oberflächen-Updates für die iOS-App laufen jetzt ohne Xcode:
@capgo/capacitor-updater eingebaut, ein Update-Abschnitt in den
Einstellungen lädt ein neues Bündel und tauscht die Oberfläche aus. Kein
Selbstlauf (autoUpdate: false) - nur auf Tastendruck, nie während der
Benutzung.

Das Bündel liegt in der Integration selbst
(custom_components/audi_dashboard/frontend/app/), nicht unter /local/: so
reist es bei jeder Installation automatisch mit, ohne zweiten
Auslieferungsweg. Gebaut von companion-app/scripts/ota-paket.ps1 (neuer
Befehl: npm run ota), gemeldet über sensor.audi_dashboard_app_version
(neues Feld daten.buendel).

Ein echter Bug beim Bauen gefunden: [IO.Compression.ZipFile]::CreateFrom-
Directory schreibt unter Windows PowerShell 5.1 Backslashes in die
Zip-Einträge - iOS hätte das Archiv falsch entpackt. Behoben, indem die
Einträge von Hand mit "/" geschrieben werden.

Rückfallebene: notifyAppReady() läuft erst, wenn React nachweislich
gerendert hat (App.tsx). Kommt diese Meldung nicht, rollt das Plugin nach
20 Sekunden von selbst auf das vorherige Bündel zurück.

Am laufenden Testcontainer verifiziert: die ausgelieferte Zip hasht exakt
auf den in bundle.json hinterlegten Wert, 13 Einträge, index.html in der
Wurzel, keine Backslashes, keine Beschädigung. tsc sauber, 117/117 Tests
(5 davon neu für buendelPasst() - dabei eine echte Lücke gefunden: die
Funktion hätte bei unbekannter eigener Version fälschlich ein Update
angeboten, jetzt genauso vorsichtig wie versionVergleichen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:20:14 +02:00

154 lines
6.4 KiB
PowerShell

<#
.SYNOPSIS
Packt die gebaute Companion-App als OTA-Bündel in die Integration.
.BESCHREIBUNG
Erzeugt aus companion-app/dist zwei Dateien:
custom_components/audi_dashboard/frontend/app/bundle.zip
custom_components/audi_dashboard/frontend/app/bundle.json
Die iOS-App lädt die Zip zur Laufzeit nach und tauscht ihre Oberfläche aus,
ohne dass Xcode gebraucht wird (siehe companion-app/src/daten/ota.ts).
WARUM DAS BÜNDEL IN DER INTEGRATION LIEGT
-----------------------------------------
Weil es dann von selbst dorthin kommt, wo die App es sucht. install.ps1
(der einzige Installationsweg - HACS scheidet aus, siehe VERSIONIERUNG.md)
kopiert den Ordner custom_components/audi_dashboard/ vollständig - das
Bündel reist bei jedem Lauf mit, ohne zweiten Auslieferungsweg. Ein
Ablageort unter /config/www/ hätte einen eigenen Weg dorthin gebraucht, den
man beim Ausliefern hätte vergessen können.
Der Nebeneffekt ist der eigentliche Gewinn: Bündel und Integration können
gar nicht auseinanderlaufen, weil sie dieselbe Datei sind.
DREI DINGE, DIE HIER BEWUSST SO SIND
------------------------------------
1. **Sourcemaps fliegen raus.** Sie machen rund zwei Drittel von dist/ aus
(1,4 von 2,0 MB) und werden auf dem Gerät nie gebraucht. Sie mitzuladen
hieße, bei jedem Update das Dreifache über die Leitung zu schicken.
2. **Die Einträge werden von Hand geschrieben, nicht von einer Bequem-API.**
Die Zip-Spezifikation verlangt "/" als Trennzeichen. Windows PowerShell
5.1 läuft auf dem .NET Framework, und dort schreiben SOWOHL
Compress-Archive ALS AUCH [IO.Compression.ZipFile]::CreateFromDirectory
Backslashes in die Einträge von Unterordnern (in .NET Core behoben, hier
aber nicht verfügbar). Nachgemessen: die erste Fassung dieses Skripts
erzeugte "assets\index-*.js" statt "assets/index-*.js" - iOS hätte daraus
Dateien mit Backslash im Namen gemacht statt einen Ordner, und die App
wäre mit fehlenden Assets gestartet.
Deshalb ZipFile::Open plus CreateEntryFromFile mit selbst gebautem
Eintragsnamen: dann steht genau das im Archiv, was hier steht. Der Inhalt
liegt ohne umschließenden Ordner in der Wurzel, index.html also direkt
dort - genau das erwartet das Plugin; fehlt es, wirft set() "no index.html
file inside the bundle folder".
3. **SHA-256 über die Zip, klein geschrieben.** Das Plugin rechnet auf dem
Gerät denselben Hash über die heruntergeladene Datei (calcChecksum in
CapgoUpdater.swift) und verwirft das Bündel bei Abweichung. Ein
abgeschnittener Download oder eine halb gespiegelte Datei fällt damit
auf, bevor die App sie startet.
.PARAMETER Bauen
Vorher "npm run build" ausführen. Ohne das wird der Inhalt von dist/
genommen, wie er gerade daliegt.
.BEISPIEL
.\ota-paket.ps1 -Bauen
npm run ota
#>
param(
[switch]$Bauen
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$hier = $PSScriptRoot
$app = Split-Path $hier -Parent
$projekt = Split-Path $app -Parent
$dist = Join-Path $app "dist"
$manifest = Join-Path $projekt "custom_components\audi_dashboard\manifest.json"
$ziel = Join-Path $projekt "custom_components\audi_dashboard\frontend\app"
function Gut($t) { Write-Host " [ok] $t" -ForegroundColor Green }
function Info($t) { Write-Host " [info] $t" -ForegroundColor DarkGray }
Write-Host ""
Write-Host " OTA-Bündel bauen" -ForegroundColor White
Write-Host " ================" -ForegroundColor White
if ($Bauen) {
Info "npm run build ..."
Push-Location $app
try { npm run build; if ($LASTEXITCODE -ne 0) { throw "npm run build fehlgeschlagen" } }
finally { Pop-Location }
}
if (-not (Test-Path (Join-Path $dist "index.html"))) {
throw "$dist\index.html fehlt - erst 'npm run build' laufen lassen (oder -Bauen benutzen)."
}
$version = (Get-Content $manifest -Raw | ConvertFrom-Json).version
if (-not $version) { throw "Kein 'version'-Feld in $manifest" }
Gut "Version: $version"
# In einen Zwischenordner spiegeln, damit die Sourcemaps nicht mitwandern -
# dist/ selbst bleibt unangetastet (die Maps sind beim Entwickeln nützlich).
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("dm360-ota-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
Copy-Item "$dist\*" $tmp -Recurse -Force
$maps = Get-ChildItem $tmp -Recurse -File -Filter "*.map"
$maps | ForEach-Object { Remove-Item $_.FullName -Force }
Info "$($maps.Count) Sourcemap(s) ausgelassen"
if (-not (Test-Path (Join-Path $tmp "index.html"))) {
throw "index.html liegt nicht in der Wurzel des Bündels - das Plugin würde es ablehnen."
}
New-Item -ItemType Directory -Force -Path $ziel | Out-Null
$zip = Join-Path $ziel "bundle.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
# Eintragsnamen selbst bauen (siehe Punkt 2 im Kopfkommentar): relativer
# Pfad ab dem Zwischenordner, Backslashes durch "/" ersetzt.
$archiv = [IO.Compression.ZipFile]::Open($zip, [System.IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($datei in Get-ChildItem $tmp -Recurse -File | Sort-Object FullName) {
$name = $datei.FullName.Substring($tmp.Length + 1).Replace("\", "/")
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archiv, $datei.FullName, $name, [IO.Compression.CompressionLevel]::Optimal)
}
}
finally { $archiv.Dispose() }
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
$groesse = (Get-Item $zip).Length
Gut ("bundle.zip {0:N0} Bytes" -f $groesse)
Gut "sha256 $hash"
# Kleingeschriebener Hex-Hash, weil das Plugin auf dem Geraet ebenfalls
# kleingeschrieben vergleicht.
$daten = [ordered]@{
version = $version
sha256 = $hash
bytes = $groesse
gebaut = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
}
$json = ($daten | ConvertTo-Json -Compress)
[IO.File]::WriteAllText((Join-Path $ziel "bundle.json"), $json, (New-Object Text.UTF8Encoding($false)))
Gut "bundle.json geschrieben"
}
finally {
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host ""
Write-Host " Fertig. Das Bündel wird mit der Integration ausgeliefert -" -ForegroundColor Green
Write-Host " homeassistant\installationspaket\Installieren.cmd nimmt es automatisch mit."
Write-Host ""