Ö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:
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user