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>
This commit is contained in:
2026-08-24 09:20:14 +02:00
parent d8b12da36d
commit 1354ca6b06
22 changed files with 695 additions and 99 deletions
@@ -37,6 +37,14 @@ PANEL_KOMPONENTE = "audi-dashboard-panel"
STATIK_URL = "/audi_dashboard_static"
STATIK_ORDNER = "frontend"
# Das OTA-Bündel der iOS-App: dieselbe Oberfläche, als Zip zum Nachladen.
# Liegt bewusst mit im Integrationsordner - dann liefert HACS es bei jedem
# Update mit aus, und Bündel und Integration können gar nicht auseinander-
# laufen (siehe companion-app/scripts/ota-paket.ps1).
BUENDEL_ORDNER = "frontend/app"
BUENDEL_URL = f"{STATIK_URL}/app/bundle.zip"
BUENDEL_INFO = "bundle.json"
# Vom Nutzer hochgeladene Fahrzeugfotos. Die bleiben in /config/www/bilder,
# also unter /local/bilder/: sie sind NUTZERDATEN, kein Auslieferbestandteil.
# Lägen sie im Integrationsordner, würde das nächste HACS-Update sie
@@ -0,0 +1 @@
{"version":"2026.8.24.1","sha256":"967ab36fc866187cf5fdc03d95a3ec21cb6e42003d8c012f8f67b99c67373324","bytes":230770,"gebaut":"2026-08-24T07:16:44Z"}
@@ -30,6 +30,7 @@ from __future__ import annotations
import asyncio
import datetime
import json
import logging
import os
from collections.abc import Callable, Coroutine
@@ -49,6 +50,9 @@ from homeassistant.helpers.storage import Store
from . import batterie, fahrterkennung, reifen, screening, sicherung, tankerkennung
from .ablage import Ablage
from .const import (
BUENDEL_INFO,
BUENDEL_ORDNER,
BUENDEL_URL,
E_APP_VERSION,
E_BATTERIEVERLAUF,
E_BELEG_ERGEBNIS,
@@ -303,8 +307,35 @@ class Koordinator:
async def import_status_veroeffentlichen(self, zustand: str, daten: dict) -> None:
self.setzen(E_IMPORT_STATUS, zustand, daten)
def _buendel_lesen(self) -> dict | None:
"""Angaben zum mitgelieferten OTA-Bündel, oder None.
None ist ein völlig normaler Zustand: wer nur das Panel benutzt,
braucht kein App-Bündel, und eine Installation ohne bundle.json ist
deshalb nicht kaputt, sondern schlicht ohne diese Möglichkeit."""
pfad = os.path.join(
os.path.dirname(__file__), BUENDEL_ORDNER.replace("/", os.sep), BUENDEL_INFO
)
if not os.path.exists(pfad):
return None
try:
with open(pfad, encoding="utf-8") as datei:
info = json.load(datei)
except (ValueError, OSError) as fehler:
_LOGGER.warning("%s ist nicht lesbar (%s)", pfad, fehler)
return None
if not isinstance(info, dict) or not info.get("version") or not info.get("sha256"):
_LOGGER.warning("%s hat nicht die erwartete Form - Bündel wird nicht gemeldet", pfad)
return None
return {**info, "url": BUENDEL_URL}
async def alles_veroeffentlichen(self) -> None:
self.setzen(E_APP_VERSION, self.version, {"app": self.version})
buendel = await self.hass.async_add_executor_job(self._buendel_lesen)
# Die App liest beides aus derselben Entität: welchen Stand diese
# Installation ausliefert, und ob ein nachladbares Bündel danebenliegt.
# Zusammen an einer Stelle, damit die App nicht zwei Quellen abgleichen
# muss, die auseinanderlaufen können.
self.setzen(E_APP_VERSION, self.version, {"app": self.version, "buendel": buendel})
await self.profil_veroeffentlichen()
await self.fahrten_veroeffentlichen()
await self.tankvorgaenge_veroeffentlichen()
@@ -1,10 +1,10 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.23.2",
"documentation": "https://github.com/paulnothaft/audi-app/blob/main/README.md",
"issue_tracker": "https://github.com/paulnothaft/audi-app/issues",
"codeowners": ["@paulnothaft"],
"version": "2026.8.24.1",
"documentation": "https://github.com/T130B/DM360/blob/main/README.md",
"issue_tracker": "https://github.com/T130B/DM360/issues",
"codeowners": ["@T130B"],
"config_flow": true,
"integration_type": "service",
"single_config_entry": true,