Files
audi-app/custom_components/audi_dashboard/bilder.py
T
tobias fb251154c7 Ölwechsel/Inspektion-Anzeige repariert, Selbst-Update der Integration gebaut
Zwei getrennte Themen in einem Commit, beide in derselben Sitzung entstanden:

1. Ölwechsel/Inspektion wurden komplett ausgeblendet ("kein Eintrag im
   Servicebuch"), sobald kein Servicebucheintrag vorlag - selbst wenn der
   zugeordnete Sensor eine gültige Fälligkeit meldete. In beiden Frontends
   prüfte die Anzeige nur den Servicebuch-Zweig, bevor sie die
   Fahrzeugmeldung überhaupt las. Jetzt steht die Fahrzeugmeldung für sich;
   fehlt zusätzlich ein Servicebucheintrag, übernimmt eine neue,
   fahrtenlog-basierte Prognose (kmProTagAusFahrten()/meldungsPrognose())
   die Hochrechnung statt der Servicebuch-Rate - deckelt auf die vom
   Fahrzeug selbst gemeldete Zeitgrenze, falls zu wenig gefahren wird.

2. install.ps1 als Update-Weg wird von Windows Smart App Control blockiert,
   ohne Umgehungsmöglichkeit. Die Integration lädt sich jetzt auf
   Tastendruck selbst von Gitea (aktualisierung.py), verifiziert das
   Manifest vor jedem Tausch und tauscht per os.rename mit automatischem
   Rollback bei Fehlern - install.ps1 bleibt nur noch für die
   Erstinstallation nötig. Zugangstoken über einen neuen OptionsFlow in
   entry.options, nie in configuration.yaml.

Nebenbei: mehrere seit der HACS-Ausschluss-Entscheidung liegen gebliebene
falsche HACS-Referenzen in Code-Kommentaren und einem UI-Text korrigiert.

Details, Sicherheitsbegründung und Verifikationsstand in AGENTS.md,
Abschnitte I und J.

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

93 lines
3.1 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 der nächste install.ps1-Lauf sie mitlöschen (er leert den Zielordner
vor dem Kopieren, siehe install.ps1). /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)