fb251154c7
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>
93 lines
3.1 KiB
Python
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)
|