1354ca6b06
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>
154 lines
6.4 KiB
PowerShell
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 ""
|