<# .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 ""