d8b12da36d
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS. Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und "pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die Integration beim ersten Start aus ihrer Vorlage an. Drei alte Schwächen sind dabei mit erledigt: - Die Nutzlast landet nicht mehr in der Recorder-Datenbank (_unrecorded_attributes - das kann nur eine echte Entität). - Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher); fiel sie während eines Ausfalls ins Ende, schließt nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt. - Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg. Namensvertrag geändert, beide Oberflächen mitgezogen: pyscript.audi_dashboard_x -> sensor.audi_dashboard_x, pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das Panel liegt in der Integration und kann nicht driften. Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json. Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport, echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112 Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""Fahrzeugbilder direkt aus der Oberfläche hochladen und löschen
|
|
(Einstellungen -> Fahrzeugbilder).
|
|
|
|
Nur die festen, im Frontend fest verdrahteten Dateinamen sind erlaubt - eine
|
|
Positivliste, damit ein manipulierter Dateiname aus dem Frontend niemals
|
|
außerhalb von www/bilder/ schreiben kann (kein Pfad-Traversal über "../" oder
|
|
absolute Pfade).
|
|
|
|
Die Bytes werden unverändert unter dem erwarteten Namen abgelegt, unabhängig
|
|
vom tatsächlichen Bildformat des Originalfotos (z. B. JPEG von einem iPhone
|
|
unter einem ".webp"-Namen) - Browser stellen <img> nach den tatsächlichen
|
|
Bilddaten dar, nicht nach der Dateiendung. Eine Größenbeschränkung gibt es
|
|
bewusst nicht: jede Fotogröße muss hochladbar sein.
|
|
|
|
WARUM DIESE DATEIEN NICHT IM INTEGRATIONSORDNER LIEGEN: sie sind Nutzerdaten,
|
|
kein Auslieferbestandteil. Lägen sie unter custom_components/audi_dashboard/,
|
|
würde das nächste HACS-Update sie mitlöschen. /config/www/bilder/ gehört
|
|
dagegen dem Nutzer und wird von keinem Update angefasst; ausgeliefert wird es
|
|
weiterhin unter /local/bilder/.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import binascii
|
|
import logging
|
|
import os
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ERLAUBTE_DATEINAMEN = frozenset({
|
|
"seitenansicht.webp",
|
|
"seitenansicht-winter.webp",
|
|
"front-schraeg.webp",
|
|
"frontansicht.webp",
|
|
"heckansicht.webp",
|
|
"cockpit.webp",
|
|
"scheinwerfer.webp",
|
|
"sitze.webp",
|
|
"rad-sommer.webp",
|
|
"rad-winter.webp",
|
|
"draufsicht.webp",
|
|
})
|
|
|
|
|
|
def _pfad(ordner: str, dateiname: str | None) -> str | None:
|
|
if not dateiname or dateiname not in ERLAUBTE_DATEINAMEN:
|
|
return None
|
|
return os.path.join(ordner, dateiname)
|
|
|
|
|
|
def _schreiben(pfad: str, rohdaten: bytes) -> None:
|
|
os.makedirs(os.path.dirname(pfad), exist_ok=True)
|
|
tmp = pfad + ".tmp"
|
|
with open(tmp, "wb") as datei:
|
|
datei.write(rohdaten)
|
|
os.replace(tmp, pfad)
|
|
|
|
|
|
def _loeschen(pfad: str) -> bool:
|
|
if not os.path.exists(pfad):
|
|
return False
|
|
os.remove(pfad)
|
|
return True
|
|
|
|
|
|
async def hochladen(
|
|
hass: HomeAssistant, ordner: str, dateiname: str | None, daten_base64: str | None
|
|
) -> None:
|
|
pfad = _pfad(ordner, dateiname)
|
|
if not pfad or not daten_base64:
|
|
_LOGGER.warning("Bild-Upload abgelehnt (dateiname=%s)", dateiname)
|
|
return
|
|
try:
|
|
rohdaten = base64.b64decode(daten_base64)
|
|
except (binascii.Error, ValueError) as fehler:
|
|
_LOGGER.error("Bild-Upload: Daten nicht lesbar (%s)", fehler)
|
|
return
|
|
await hass.async_add_executor_job(_schreiben, pfad, rohdaten)
|
|
_LOGGER.info("Fahrzeugbild gespeichert (%s, %s Bytes)", dateiname, len(rohdaten))
|
|
|
|
|
|
async def loeschen(hass: HomeAssistant, ordner: str, dateiname: str | None) -> None:
|
|
pfad = _pfad(ordner, dateiname)
|
|
if not pfad:
|
|
_LOGGER.warning("Bild-Löschen abgelehnt (dateiname=%s)", dateiname)
|
|
return
|
|
if await hass.async_add_executor_job(_loeschen, pfad):
|
|
_LOGGER.info("Fahrzeugbild gelöscht (%s)", dateiname)
|