Ö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
@@ -38,9 +38,11 @@ export function Einstellungen({
tankvorgaenge,
rohprofil,
otaBuendel,
integrationUpdate,
api,
profilSpeichern,
jetztAktualisieren,
neuLaden,
} = useDaten()
const [theme, themeSetzen] = useTheme()
const [entwurf, setzeEntwurf] = useState<EinstellungenWerte | null>(null)
@@ -49,6 +51,8 @@ export function Einstellungen({
const [importOffen, setzeImportOffen] = useState(false)
const [otaLaeuft, setzeOtaLaeuft] = useState(false)
const [otaFehler, setzeOtaFehler] = useState<string | null>(null)
const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false)
const [updateInstallierenLaeuft, setzeUpdateInstallierenLaeuft] = useState(false)
const dateiwahl = useRef<HTMLInputElement | null>(null)
const otaUpdateVerfuegbar = otaMoeglich() && buendelPasst(otaBuendel, eigeneVersion())
@@ -72,6 +76,33 @@ export function Einstellungen({
}
}
// Selbst-Update der Integration (aktualisierung.py) - Ersatz für
// install.ps1, das Windows Smart App Control blockiert. Beide Aktionen
// laufen sofort (keine Warteschlange, siehe api/index.ts) und laden
// danach über neuLaden() den neuen Stand von
// sensor.audi_dashboard_app_version - der Fehlertext bei fehlendem Token
// kommt von dort mit, eine eigene "ist ein Token eingetragen?"-Prüfung
// gibt es bewusst nicht.
const updatePruefenAusloesen = async () => {
setzeUpdatePruefenLaeuft(true)
try {
await api.updatePruefen()
await neuLaden()
} finally {
setzeUpdatePruefenLaeuft(false)
}
}
const updateInstallierenAusloesen = async () => {
setzeUpdateInstallierenLaeuft(true)
try {
await api.updateInstallieren()
await neuLaden()
} finally {
setzeUpdateInstallierenLaeuft(false)
}
}
if (!einstellungen || !fahrzeug) return null
const werte = entwurf ?? einstellungen
@@ -410,6 +441,56 @@ export function Einstellungen({
</Tile>
)}
<Tile>
<span className="ads-eyebrow">Integration-Update</span>
{integrationUpdate?.installiert ? (
<>
<p className="dm-fussnote">
Version {integrationUpdate.version} installiert. Home Assistant neu starten, damit
die neue Fassung geladen wird Einstellungen System Neu starten.
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}>
{updatePruefenLaeuft ? "Prüfe …" : "Erneut prüfen"}
</ActionButton>
</div>
</>
) : integrationUpdate?.verfuegbar ? (
<>
<Werteliste
kinder={<Wertzeile label="Verfügbare Version" wert={integrationUpdate.version ?? "?"} />}
/>
<p className="dm-fussnote">
Lädt die neueste Fassung von Gitea und ersetzt den Integrationsordner. Home Assistant
muss danach neu gestartet werden. Fahrzeugprofil, Fahrten und Tankvorgänge sind davon
nicht betroffen.
</p>
<div className="dm-knopfreihe">
<ActionButton
onClick={() => void updateInstallierenAusloesen()}
disabled={updateInstallierenLaeuft}
>
{updateInstallierenLaeuft ? "Installiere …" : "Update installieren"}
</ActionButton>
</div>
</>
) : (
<>
<p className="dm-fussnote">
{integrationUpdate
? `Integration ist aktuell (${integrationUpdate.geprueft_am ? datumZeit(integrationUpdate.geprueft_am) : ""}).`
: "Noch nicht geprüft."}
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}>
{updatePruefenLaeuft ? "Prüfe …" : "Auf Update prüfen"}
</ActionButton>
</div>
</>
)}
{integrationUpdate?.fehler && <p className="dm-fehler">{integrationUpdate.fehler}</p>}
</Tile>
<Tile>
<span className="ads-eyebrow">Zugang</span>
<Werteliste
+27 -6
View File
@@ -13,7 +13,13 @@ import { useState } from "react"
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
import { useDaten } from "../daten/DatenKontext"
import { inspektionPrognose, letzterInspektion, letzterOelwechsel, oelwechselPrognose } from "../daten/service"
import {
inspektionPrognose,
letzterInspektion,
letzterOelwechsel,
meldungsPrognose,
oelwechselPrognose,
} from "../daten/service"
import { datum, de, isoTag } from "../format"
import type { SeitenName } from "../navigation"
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
@@ -37,7 +43,7 @@ interface Autohaus {
}
export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
const { fahrzeug } = useDaten()
const { fahrzeug, fahrten } = useDaten()
if (!fahrzeug) return null
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
@@ -62,6 +68,12 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
: prognose
? `${de(prognose.restKm)} km · ${datum(prognose.datum)}`
: null
// Ganz ohne Servicebucheintrag (prognose null) übernimmt die
// Fahrzeugmeldung selbst die Rolle der Prognosequelle, hochgerechnet mit
// der echten Fahrleistung aus dem Fahrtenlog statt einer Servicebuch-Rate.
const oelMeldungProg = oelHatMeldung && !prognose
? meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten)
: null
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
// Herstellerintervall - die Restkilometerzahl daneben wäre nur eine zweite,
// verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim eigenen
@@ -70,7 +82,9 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
? fahrzeug.oel.modus === "hersteller"
? `voraussichtlich am ${datum(prognose.datum)}`
: `${de(prognose.restKm)} km, voraussichtlich am ${datum(prognose.datum)}`
: null
: oelMeldungProg
? `voraussichtlich am ${datum(oelMeldungProg.datum)}`
: null
// Der Ölwechsel folgt in "Einrichten" wahlweise der Herstellervorgabe oder
// einem kürzeren eigenen Intervall (fahrzeug.oel.modus) - die Inspektion hat
// kein einstellbares Intervall, rechnet also immer mit der Herstellervorgabe
@@ -92,7 +106,14 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
: inspektionsprognose
? datum(inspektionsprognose.datum)
: null
const inspFussnote = inspektionsprognose ? `voraussichtlich am ${datum(inspektionsprognose.datum)}` : null
const inspMeldungProg = inspHatMeldung && !inspektionsprognose
? meldungsPrognose(fahrzeug.inspektionFaelligTs, fahrzeug.inspektionFaelligKm, fahrten)
: null
const inspFussnote = inspektionsprognose
? `voraussichtlich am ${datum(inspektionsprognose.datum)}`
: inspMeldungProg
? `voraussichtlich am ${datum(inspMeldungProg.datum)}`
: null
const huDatum = fahrzeug.hu ? new Date(fahrzeug.hu) : null
const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime())
@@ -107,7 +128,7 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
<SymbolOelwechsel groesse={26} />
<span>Ölwechsel</span>
</div>
{oelHatEintrag ? (
{oelHatEintrag || oelHatMeldung ? (
<>
{oelHerstellervorgabe && <div className="dm-tgvorgabe">Herstellervorgabe</div>}
<div className="dm-tgwert">{oelWert}</div>
@@ -123,7 +144,7 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
<SymbolInspektion groesse={20} />
<span>Inspektion</span>
</div>
{inspHatEintrag ? (
{inspHatEintrag || inspHatMeldung ? (
<>
<div className="dm-tgvorgabe">Herstellervorgabe</div>
<div className="dm-tgwert">{inspWert}</div>
+1 -1
View File
@@ -53,7 +53,7 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
.trim()
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
const service = naechsterService(fahrzeug, buch)
const service = naechsterService(fahrzeug, buch, fahrten)
const letzteFahrt = fahrten
.slice()