Ö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>
This commit is contained in:
2026-08-24 12:01:01 +02:00
parent 5ff33c92d9
commit fb251154c7
27 changed files with 1268 additions and 82 deletions
+20
View File
@@ -197,6 +197,26 @@ export class DataMetricApi {
}
}
/* ------------------------------------------------- Selbst-Update der
Integration (aktualisierung.py) - Ersatz für install.ps1, das Windows
Smart App Control blockiert. Bewusst NICHT über die Warteschlange, wie
historieImportieren() oben: beides sind ausdrückliche, sofortige
Aktionen, kein Zustand, den man im Funkloch absetzt und später
nachgeholt haben will. Ergebnis kommt über
appVersionAngabeLesen()/daten.integration_update zurück, nicht über den
Rückgabewert dieser Aufrufe. */
/** Fragt nur nach, ob eine neuere Fassung vorliegt - lädt nichts herunter. */
updatePruefen(): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "update_pruefen");
}
/** Lädt die neueste Fassung und ersetzt den Integrationsordner. Home
Assistant muss danach neu gestartet werden. */
updateInstallieren(): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "update_installieren");
}
/* ------------------------------------------- Import aus dem HA-Verlauf
Bewusst NICHT über die Warteschlange, anders als die übrigen
schreibenden Vorgänge: der Import ist keine Eingabe, die man im Funkloch
+16
View File
@@ -172,10 +172,26 @@ export interface Buendelangabe {
url: string;
}
/** Ergebnis der letzten "Auf Update prüfen"/"Update installieren"-Aktion
(custom_components/audi_dashboard/aktualisierung.py, dienste.py). `null`,
solange noch nie geprüft wurde - ein normaler Zustand, kein Fehler.
Ersetzt install.ps1 als Update-Weg, weil Windows Smart App Control es
zuverlässig blockiert. */
export interface IntegrationUpdateAngabe {
verfuegbar: boolean;
version: string | null;
geprueft_am: string;
/** true unmittelbar nach einer erfolgreichen Installation - dann ist ein
Neustart von Home Assistant nötig, damit die neue Fassung geladen wird. */
installiert: boolean;
fehler: string | null;
}
/** Nutzlast von sensor.audi_dashboard_app_version. */
export interface AppVersionAngabe {
app: string | null;
buendel: Buendelangabe | null;
integration_update: IntegrationUpdateAngabe | null;
}
/* -------------------------------------------------- Entitäts-Verzeichnis */
+9
View File
@@ -25,6 +25,7 @@ import {
type Buendelangabe,
type Fahrt,
type Fahrzeugstatus,
type IntegrationUpdateAngabe,
type Profil,
type Tankvorgang,
type Verbindungszustand,
@@ -50,6 +51,9 @@ export interface DatenWert {
oder null — kein Fehler, sondern der Zustand ohne Bündel oder ohne
Verbindung. */
otaBuendel: Buendelangabe | null
/** Ergebnis der letzten Selbst-Update-Prüfung/-Installation der
Integration (aktualisierung.py), oder null vor der ersten Prüfung. */
integrationUpdate: IntegrationUpdateAngabe | null
einstellungen: Einstellungen | null
fahrzeug: Fahrzeug | null
@@ -93,6 +97,9 @@ export function DatenAnbieter({
const [warteschlange, setzeWarteschlange] = useState<readonly WartenderAuftrag[]>([])
const [serverVersion, setzeServerVersion] = useState<string | null>(null)
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
null,
)
// Damit profilSpeichern immer gegen den neuesten Rohstand arbeitet, auch
// wenn zwischendurch ein Push hereinkam.
@@ -121,6 +128,7 @@ export function DatenAnbieter({
const versionsangabe = await api.appVersionAngabeLesen()
setzeServerVersion(versionsangabe?.app ?? null)
setzeOtaBuendel(versionsangabe?.buendel ?? null)
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
} catch (fehler) {
// Kein Netz ist kein Ladefehler, solange schon Daten da sind — dann
// zeigt die App den zwischengespeicherten Stand mit Offline-Hinweis.
@@ -219,6 +227,7 @@ export function DatenAnbieter({
statusStand,
versionsstand: versionVergleichen(eigeneVersion(), serverVersion).stand,
otaBuendel,
integrationUpdate,
einstellungen,
fahrzeug,
fahrten,
+110 -4
View File
@@ -1,6 +1,13 @@
import { describe, expect, it } from "vitest"
import { inspektionPrognose, letzterOelwechsel, naechsterService, oelwechselPrognose } from "./service"
import {
inspektionPrognose,
kmProTagAusFahrten,
letzterOelwechsel,
meldungsPrognose,
naechsterService,
oelwechselPrognose,
} from "./service"
import type { Fahrzeug } from "./profilAdapter"
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
@@ -128,7 +135,7 @@ describe("naechsterService", () => {
const insp = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
const erwarteteArt = oel!.datum <= insp!.datum ? "Ölwechsel" : "Inspektion"
const s = naechsterService(fahrzeug(46000), BUCH, jetzt)
const s = naechsterService(fahrzeug(46000), BUCH, [], jetzt)
expect(s).not.toBeNull()
expect(s!.art).toBe(erwarteteArt)
expect(s!.datum).toBe(erwarteteArt === "Ölwechsel" ? oel!.datum : insp!.datum)
@@ -136,7 +143,7 @@ describe("naechsterService", () => {
it("nimmt die Hauptuntersuchung auf, obwohl sie keine eigene Prognose hat", () => {
const f = { ...fahrzeug(46000), hu: "2026-09-01" }
const s = naechsterService(f, [], jetzt)
const s = naechsterService(f, [], [], jetzt)
expect(s).not.toBeNull()
expect(s!.art).toBe("Hauptuntersuchung")
expect(s!.restKm).toBeNull()
@@ -144,6 +151,105 @@ describe("naechsterService", () => {
})
it("liefert nichts ohne Servicebuch und ohne gepflegte Hauptuntersuchung", () => {
expect(naechsterService(fahrzeug(46000), [], jetzt)).toBeNull()
expect(naechsterService(fahrzeug(46000), [], [], jetzt)).toBeNull()
})
it("fällt ohne Servicebucheintrag auf die vom Fahrzeug gemeldete Ölwechsel-Fälligkeit zurück", () => {
// Ein frisch eingerichtetes Fahrzeug mit zugeordnetem Sensor, aber noch
// ganz ohne Servicebuch, muss die Meldung trotzdem zeigen - siehe
// AGENTS.md, Abschnitt H, "no inspektion and ölwechsel shown ...".
const f = { ...fahrzeug(46000), oelwechselFaelligTs: "2027-03-14T00:00:00+00:00", oelwechselFaelligKm: 11750 }
const s = naechsterService(f, [], [], jetzt)
expect(s).not.toBeNull()
expect(s!.art).toBe("Ölwechsel")
expect(s!.datum).toBe("2027-03-14")
expect(s!.restKm).toBe(11750)
})
it("fällt ohne Servicebucheintrag auf die vom Fahrzeug gemeldete Inspektions-Fälligkeit zurück", () => {
const f = { ...fahrzeug(46000), inspektionFaelligTs: "2027-01-01T00:00:00+00:00", inspektionFaelligKm: 5000 }
const s = naechsterService(f, [], [], jetzt)
expect(s).not.toBeNull()
expect(s!.art).toBe("Inspektion")
expect(s!.datum).toBe("2027-01-01")
expect(s!.restKm).toBe(5000)
})
it("bevorzugt die eigene Prognose gegenüber der Fahrzeugmeldung, wenn beide vorliegen", () => {
// Servicebucheintrag vorhanden -> oelwechselPrognose() greift; die
// Meldung (weit in der Zukunft) darf sie nicht verdrängen.
const f = { ...fahrzeug(46000), oelwechselFaelligTs: "2099-01-01T00:00:00+00:00", oelwechselFaelligKm: 99999 }
const oel = oelwechselPrognose(f, BUCH, jetzt)
expect(oel).not.toBeNull()
const s = naechsterService(f, BUCH, [], jetzt)
expect(s!.datum).not.toBe("2099-01-01")
})
})
describe("kmProTagAusFahrten", () => {
const jetzt = new Date("2026-08-11T12:00:00")
it("rechnet aus abgeschlossenen Fahrten seit der ältesten die km/Tag-Rate", () => {
// Exakt 30 Tage vor jetzt (gleiche Uhrzeit), 900 km insgesamt -> 30 km/Tag.
const fahrten = [
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
]
expect(kmProTagAusFahrten(fahrten, jetzt)).toBeCloseTo(30, 5)
})
it("ignoriert offene Fahrten und Fahrten ohne bekannte Distanz", () => {
const fahrten = [
{ ts_start: "2026-07-12T08:00:00", distance_km: 400, status: "abgeschlossen" },
{ ts_start: "2026-08-10T08:00:00", distance_km: null, status: "abgeschlossen" },
{ ts_start: "2026-08-11T08:00:00", distance_km: 9999, status: "offen" },
]
// Nur die erste Fahrt zählt -> dieselbe Rate wie ohne die beiden anderen.
expect(kmProTagAusFahrten(fahrten, jetzt)).toBe(
kmProTagAusFahrten([fahrten[0]!], jetzt),
)
})
it("liefert nichts ohne Fahrten oder ohne belastbaren Zeitraum", () => {
expect(kmProTagAusFahrten([], jetzt)).toBeNull()
expect(kmProTagAusFahrten([{ ts_start: jetzt.toISOString(), distance_km: 5, status: "abgeschlossen" }], jetzt))
.toBeNull()
})
})
describe("meldungsPrognose", () => {
const jetzt = new Date("2026-08-11T12:00:00")
// 30 km/Tag, siehe kmProTagAusFahrten-Tests oben.
const fahrten = [
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
]
it("rechnet die Restkilometer mit der Fahrleistung aus dem Fahrtenlog auf ein Datum hoch", () => {
// 300 km Rest bei 30 km/Tag -> 10 Tage.
const p = meldungsPrognose("2099-01-01T00:00:00+00:00", 300, fahrten, jetzt)
expect(p).not.toBeNull()
expect(p!.datum).toBe("2026-08-21")
expect(p!.durchZeitlimit).toBe(false)
})
it("deckelt auf die vom Fahrzeug gemeldete Zeitgrenze, wenn zu wenig gefahren wird", () => {
// 30000 km Rest bei 30 km/Tag läge Jahre in der Zukunft - die gemeldete
// Zeitgrenze (fünf Tage später) greift zuerst.
const p = meldungsPrognose("2026-08-16T00:00:00+00:00", 30000, fahrten, jetzt)
expect(p).not.toBeNull()
expect(p!.datum).toBe("2026-08-16")
expect(p!.durchZeitlimit).toBe(true)
})
it("fällt ohne Fahrtenlog auf die reine Zeitgrenze der Meldung zurück", () => {
const p = meldungsPrognose("2027-01-01T00:00:00+00:00", 5000, [], jetzt)
expect(p).not.toBeNull()
expect(p!.datum).toBe("2027-01-01")
expect(p!.durchZeitlimit).toBe(true)
})
it("liefert nichts ohne jede brauchbare Angabe", () => {
expect(meldungsPrognose(null, null, fahrten, jetzt)).toBeNull()
})
})
+76 -6
View File
@@ -126,6 +126,59 @@ export function inspektionPrognose(
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
}
interface Fahrtdaten {
ts_start: string
distance_km: number | null
status?: string
}
/** Fahrleistung pro Tag aus dem tatsächlichen Fahrtenlog, unabhängig vom
Servicebuch - Grundlage für meldungsPrognose() unten, die auch ganz ohne
Servicebucheintrag funktionieren soll. Nur abgeschlossene Fahrten mit
bekannter Distanz zählen; die Rate ist der Durchschnitt seit der ältesten
erfassten Fahrt, kein kurzes Zeitfenster - damit bleibt sie robust gegen
einzelne fahrtenarme Tage. null ohne belastbare Datenlage. */
export function kmProTagAusFahrten(fahrten: readonly Fahrtdaten[], jetzt = new Date()): number | null {
const gueltig = fahrten.filter((f) => f.status !== "offen" && f.distance_km != null)
if (!gueltig.length) return null
const km = gueltig.reduce((s, f) => s + (f.distance_km ?? 0), 0)
const aeltesteZeit = Math.min(...gueltig.map((f) => new Date(f.ts_start).getTime()))
const tage = (jetzt.getTime() - aeltesteZeit) / TAG_MS
return tage >= 1 ? km / tage : null
}
export interface Meldungsprognose {
/** ISO-Tagesdatum. */
datum: string
/** Die vom Fahrzeug gemeldete Zeitgrenze hat gegriffen, nicht die
hochgerechnete Kilometergrenze - der Nutzer fährt zu wenig, um die
km-Grenze rechtzeitig zu erreichen. */
durchZeitlimit: boolean
}
/** Prognose rein aus der Fahrzeugmeldung, für den Fall ganz ohne
Servicebucheintrag (siehe Aufrufer unten und in Service.tsx) - ein echter
Servicebucheintrag bleibt vorrangig, näher am tatsächlichen letzten
Service. Restkilometer aus der Meldung, hochgerechnet mit der echten
Fahrleistung aus dem Fahrtenlog statt einer Servicebuch-Rate; gedeckelt
auf das vom Fahrzeug selbst gemeldete Datum als Zeitgrenze. */
export function meldungsPrognose(
faelligTs: string | null,
faelligKm: number | null,
fahrten: readonly Fahrtdaten[],
jetzt = new Date(),
): Meldungsprognose | null {
const zeitDatum = faelligTs ? faelligTs.slice(0, 10) : null
if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
const restKm = Math.abs(faelligKm)
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false }
const rate = kmProTagAusFahrten(fahrten, jetzt)
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
const kmDatum = new Date(jetzt.getTime() + (restKm / rate) * TAG_MS).toISOString().slice(0, 10)
const durchZeitlimit = !!zeitDatum && zeitDatum < kmDatum
return { datum: durchZeitlimit ? (zeitDatum as string) : kmDatum, durchZeitlimit }
}
/* ------------------------------------------------------ Nächster Service */
export type Serviceart = "Ölwechsel" | "Inspektion" | "Hauptuntersuchung"
@@ -143,23 +196,40 @@ export interface NaechsterService {
* Der zeitlich nächste anstehende Service über alle drei Arten hinweg —
* Gegenstück zu `naechsterTermin()` im Panel.
*
* Ölwechsel und Inspektion kommen aus der eigenen Prognose (Servicebuch plus
* tatsächliche Fahrleistung), die Hauptuntersuchung aus dem im Profil
* gepflegten Termin: sie hat kein Kilometerziel, das Fahrzeug meldet sie nicht,
* und hochrechnen lässt sie sich auch nicht.
* Ölwechsel und Inspektion kommen vorrangig aus der eigenen Prognose
* (Servicebuch plus tatsächliche Fahrleistung); ohne Servicebucheintrag dazu
* fällt jede der beiden einzeln auf meldungsPrognose() zurück - die vom
* Fahrzeug selbst gemeldete Fälligkeit, hochgerechnet mit der Fahrleistung
* aus dem Fahrtenlog. Ein frisch eingerichtetes Fahrzeug mit zugeordnetem
* Sensor, aber noch ganz ohne Servicebuch, zeigte hier vorher "kein Eintrag
* im Servicebuch", obwohl die Fälligkeit längst bekannt war. Die
* Hauptuntersuchung kommt weiterhin nur aus dem im Profil gepflegten
* Termin: sie hat kein Kilometerziel, das Fahrzeug meldet sie nicht, und
* hochrechnen lässt sie sich auch nicht.
*/
export function naechsterService(
fahrzeug: Fahrzeug,
buch: readonly Buchhaltung[],
fahrten: readonly Fahrtdaten[] = [],
jetzt = new Date(),
): NaechsterService | null {
const kandidaten: NaechsterService[] = []
const oel = oelwechselPrognose(fahrzeug, buch, jetzt)
if (oel) kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
if (oel) {
kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
} else {
const m = meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten, jetzt)
if (m) kandidaten.push({ art: "Ölwechsel", restKm: fahrzeug.oelwechselFaelligKm, datum: m.datum })
}
const insp = inspektionPrognose(fahrzeug, buch, jetzt)
if (insp) kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
if (insp) {
kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
} else {
const m = meldungsPrognose(fahrzeug.inspektionFaelligTs, fahrzeug.inspektionFaelligKm, fahrten, jetzt)
if (m) kandidaten.push({ art: "Inspektion", restKm: fahrzeug.inspektionFaelligKm, datum: m.datum })
}
// fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.
const hu = new Date(fahrzeug.hu)
@@ -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()