Ö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 <[email protected]>
This commit is contained in:
2026-08-24 12:01:01 +02:00
co-authored by Claude Sonnet 5
parent 5ff33c92d9
commit fb251154c7
27 changed files with 1268 additions and 82 deletions
@@ -1,25 +1,34 @@
"""Einrichtung über die Oberfläche (Einstellungen -> Geräte & Dienste ->
Integration hinzufügen -> Audi Dashboard).
Bewusst ohne Eingabefelder. Es gäbe genau eine Sache zu fragen - welche
Entität welche Rolle im Fahrzeug spielt -, und die gehört nicht hierher: die
Zuordnung wird im Setup-Menü der App selbst vorgenommen, wo neben jedem Feld
steht, wofür es gebraucht wird und was passiert, wenn es leer bleibt. Sie
hier abzufragen hieße, dieselbe Auswahl an zwei Stellen zu pflegen - und die
schlechtere von beiden zuerst zu zeigen.
Bewusst ohne Eingabefelder im Ersteinrichtungs-Schritt. Es gäbe genau eine
Sache zu fragen - welche Entität welche Rolle im Fahrzeug spielt -, und die
gehört nicht hierher: die Zuordnung wird im Setup-Menü der App selbst
vorgenommen, wo neben jedem Feld steht, wofür es gebraucht wird und was
passiert, wenn es leer bleibt. Sie hier abzufragen hieße, dieselbe Auswahl an
zwei Stellen zu pflegen - und die schlechtere von beiden zuerst zu zeigen.
Eine einzige Instanz, erzwungen über single_config_entry in manifest.json:
die App gehört zu genau einem Fahrzeug, und alle Bestände liegen unter einem
festen Pfad.
Der OptionsFlow unten ist die einzige Ausnahme: ein Gitea-Zugriffstoken für
das Selbst-Update (siehe aktualisierung.py) - ein echtes Geheimnis, das
deshalb in HAs eigenem, verschlüsseltem Config-Entry-Speicher landet
(entry.options), nicht in configuration.yaml.
"""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
import voluptuous as vol
from .const import DOMAIN
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.core import callback
from homeassistant.helpers import selector
from .const import CONF_GITEA_TOKEN, DOMAIN
class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -35,3 +44,35 @@ class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is None:
return self.async_show_form(step_id="user")
return self.async_create_entry(title="Audi Dashboard", data={})
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
return AudiDashboardOptionsFlow()
class AudiDashboardOptionsFlow(OptionsFlow):
"""Einstellungen -> Geräte & Dienste -> Audi Dashboard -> Konfigurieren.
Ein Feld: der Gitea-Token fürs Selbst-Update. Leer lassen deaktiviert
die Update-Prüfung wieder (aktualisierung.py verlangt einen nicht-leeren
Token, sonst gibt es eine verständliche Fehlermeldung statt eines
unbehandelten Fehlers)."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({
vol.Optional(CONF_GITEA_TOKEN, default=""): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
}),
self.config_entry.options,
),
)