Selbst-Update-Sicherung, Service-Prognose ohne Servicebuch und Sicherheit neu gestaltet
- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern, sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers). - service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert; eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch- eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt weiterhin unveraendert die Herstellervorgabe zu zeigen. - Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor- rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/ Kofferraum in beiden Frontends. Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,7 @@ describe("profilZuFahrzeug mit Fahrzeugstatus", () => {
|
||||
tankprozent: 62,
|
||||
reichweite_km: 385,
|
||||
gesichert: true,
|
||||
sicherheitscheck: [{ label: "Tür vorne links", ok: true }],
|
||||
sicherheitscheck: [{ label: "Tür vorne links", ok: true, gruppe: "tueren_klappen" }],
|
||||
})
|
||||
expect(f.odo).toBe(48250)
|
||||
expect(f.odoBekannt).toBe(true)
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
kmProTagAusFahrten,
|
||||
letzterOelwechsel,
|
||||
meldungsPrognose,
|
||||
meldungsPrognoseEigenesIntervall,
|
||||
naechsterService,
|
||||
oelMeldungsPrognose,
|
||||
oelwechselPrognose,
|
||||
} from "./service"
|
||||
import type { Fahrzeug } from "./profilAdapter"
|
||||
@@ -252,4 +254,96 @@ describe("meldungsPrognose", () => {
|
||||
it("liefert nichts ohne jede brauchbare Angabe", () => {
|
||||
expect(meldungsPrognose(null, null, fahrten, jetzt)).toBeNull()
|
||||
})
|
||||
|
||||
it("gibt die Restkilometer mit zurück", () => {
|
||||
const p = meldungsPrognose("2027-01-01T00:00:00+00:00", 5000, [], jetzt)
|
||||
expect(p!.restKm).toBe(5000)
|
||||
})
|
||||
})
|
||||
|
||||
describe("meldungsPrognoseEigenesIntervall", () => {
|
||||
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 Restkilometer und Zeitgrenze auf das eigene, kürzere Intervall um", () => {
|
||||
// Herstellermeldung: 25.000 km / 24 Monate Rest bis zum 2027-08-11 -
|
||||
// daraus ergibt sich ein angenommener letzter Service vor 2025-08-11.
|
||||
// Eigenes Intervall (15.000 km / 12 Monate) ab demselben Punkt: Zeit-
|
||||
// grenze 2026-08-11 (heute), Restkilometer 25000 - 30000 + 15000 = 10000.
|
||||
const p = meldungsPrognoseEigenesIntervall(
|
||||
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, fahrten, jetzt,
|
||||
)
|
||||
expect(p).not.toBeNull()
|
||||
expect(p!.restKm).toBe(10000)
|
||||
// 10.000 km bei 30 km/Tag läge weit in der Zukunft - die zurückgerechnete
|
||||
// eigene Zeitgrenze (heute) greift zuerst.
|
||||
expect(p!.durchZeitlimit).toBe(true)
|
||||
expect(p!.datum).toBe("2026-08-11")
|
||||
})
|
||||
|
||||
it("deckelt die Restkilometer bei null, wenn das eigene Intervall rechnerisch schon überschritten ist", () => {
|
||||
// Erst 10.000 von 30.000 km seit der Herstellerfälligkeit gefahren -
|
||||
// das kürzere eigene Intervall (15.000 km) ist rechnerisch längst fällig.
|
||||
const p = meldungsPrognoseEigenesIntervall(
|
||||
"2027-08-11T00:00:00+00:00", 10000, 30000, 24, 15000, 12, fahrten, jetzt,
|
||||
)
|
||||
expect(p).not.toBeNull()
|
||||
expect(p!.restKm).toBe(0)
|
||||
expect(p!.durchZeitlimit).toBe(false)
|
||||
})
|
||||
|
||||
it("fällt ohne Fahrtenlog auf die zurückgerechnete eigene Zeitgrenze zurück", () => {
|
||||
const p = meldungsPrognoseEigenesIntervall(
|
||||
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, [], jetzt,
|
||||
)
|
||||
expect(p).not.toBeNull()
|
||||
expect(p!.datum).toBe("2026-08-11")
|
||||
expect(p!.durchZeitlimit).toBe(true)
|
||||
})
|
||||
|
||||
it("liefert nichts ohne bekannte Herstellerintervall-Länge", () => {
|
||||
expect(
|
||||
meldungsPrognoseEigenesIntervall("2027-08-11T00:00:00+00:00", 25000, null, 24, 15000, 12, fahrten, jetzt),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("oelMeldungsPrognose", () => {
|
||||
const jetzt = new Date("2026-08-11T12:00:00")
|
||||
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("übernimmt bei Herstellervorgabe unverändert die reine Fahrzeugmeldung", () => {
|
||||
const f = fahrzeug(10000, "hersteller")
|
||||
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
||||
f.oelwechselFaelligKm = 25000
|
||||
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
||||
expect(p!.restKm).toBe(25000)
|
||||
expect(p!.datum).toBe("2027-08-11")
|
||||
})
|
||||
|
||||
it("rechnet bei eigenem Intervall auf dessen kürzere Vorgabe um", () => {
|
||||
const f = fahrzeug(10000, "eigen")
|
||||
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
||||
f.oelwechselFaelligKm = 25000
|
||||
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
||||
expect(p!.restKm).toBe(10000)
|
||||
expect(p!.datum).toBe("2026-08-11")
|
||||
})
|
||||
|
||||
it("fällt bei eigenem Intervall ohne Herstellerwerte auf die reine Meldung zurück", () => {
|
||||
const f = fahrzeug(10000, "eigen")
|
||||
f.oel.herstellerKm = null
|
||||
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
||||
f.oelwechselFaelligKm = 25000
|
||||
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
||||
expect(p!.restKm).toBe(25000)
|
||||
expect(p!.datum).toBe("2027-08-11")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -154,6 +154,8 @@ export interface Meldungsprognose {
|
||||
hochgerechnete Kilometergrenze - der Nutzer fährt zu wenig, um die
|
||||
km-Grenze rechtzeitig zu erreichen. */
|
||||
durchZeitlimit: boolean
|
||||
/** Restkilometer bis zur Fälligkeit, falls bekannt. */
|
||||
restKm: number | null
|
||||
}
|
||||
|
||||
/** Prognose rein aus der Fahrzeugmeldung, für den Fall ganz ohne
|
||||
@@ -169,14 +171,74 @@ export function meldungsPrognose(
|
||||
jetzt = new Date(),
|
||||
): Meldungsprognose | null {
|
||||
const zeitDatum = faelligTs ? faelligTs.slice(0, 10) : null
|
||||
if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
|
||||
if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm: null } : null
|
||||
const restKm = Math.abs(faelligKm)
|
||||
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false }
|
||||
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
|
||||
const rate = kmProTagAusFahrten(fahrten, jetzt)
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : 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 }
|
||||
return { datum: durchZeitlimit ? (zeitDatum as string) : kmDatum, durchZeitlimit, restKm }
|
||||
}
|
||||
|
||||
/** Eigenes (kürzeres) Intervall ganz ohne Servicebucheintrag: die
|
||||
Fahrzeugmeldung kennt nur das Herstellerintervall, nicht die in
|
||||
"Einrichten" gewählte kürzere Vorgabe. Ein angenommener letzter Service
|
||||
wird aus der gemeldeten Fälligkeit und der bekannten Herstellerintervall-
|
||||
Länge zurückgerechnet ("Fälligkeitspunkt minus Herstellerintervall"), und
|
||||
das eigene Intervall ab demselben angenommenen Punkt neu hochgerechnet -
|
||||
eine Näherung (der wahre letzte Service kann näher oder ferner liegen),
|
||||
aber ohne sie würde die eigene Intervallwahl bis zum ersten
|
||||
Servicebucheintrag komplett ignoriert (siehe oelMeldungsPrognose()). */
|
||||
export function meldungsPrognoseEigenesIntervall(
|
||||
faelligTs: string | null,
|
||||
faelligKm: number | null,
|
||||
herstellerKm: number | null,
|
||||
herstellerMonate: number | null,
|
||||
eigenKm: number | null,
|
||||
eigenMonate: number | null,
|
||||
fahrten: readonly Fahrtdaten[],
|
||||
jetzt = new Date(),
|
||||
): Meldungsprognose | null {
|
||||
if (faelligKm == null || !herstellerKm || eigenKm == null) return null
|
||||
const restKm = Math.max(0, Math.abs(faelligKm) - herstellerKm + eigenKm)
|
||||
const zeitDatum =
|
||||
faelligTs && herstellerMonate != null && eigenMonate != null
|
||||
? monatePlus(faelligTs, eigenMonate - herstellerMonate)
|
||||
: null
|
||||
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
|
||||
const rate = kmProTagAusFahrten(fahrten, jetzt)
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : 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, restKm }
|
||||
}
|
||||
|
||||
/** Fällt ganz ohne Servicebucheintrag auf die Fahrzeugmeldung zurück - bei
|
||||
Herstellervorgabe unverändert (die Meldung IST die Herstellervorgabe),
|
||||
bei eigenem (kürzerem) Intervall über meldungsPrognoseEigenesIntervall()
|
||||
hochgerechnet, damit die Intervallwahl in "Einrichten" auch ganz ohne
|
||||
Servicebucheintrag etwas bewirkt. Fehlen die Herstellerwerte selbst
|
||||
(kein Sensor dafür), fällt das sanft auf die reine Meldung zurück. */
|
||||
export function oelMeldungsPrognose(
|
||||
fahrzeug: Fahrzeug,
|
||||
fahrten: readonly Fahrtdaten[],
|
||||
jetzt = new Date(),
|
||||
): Meldungsprognose | null {
|
||||
const oel = fahrzeug.oel
|
||||
const roh = () => meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten, jetzt)
|
||||
if (oel.modus === "hersteller") return roh()
|
||||
const eigen = meldungsPrognoseEigenesIntervall(
|
||||
fahrzeug.oelwechselFaelligTs,
|
||||
fahrzeug.oelwechselFaelligKm,
|
||||
oel.herstellerKm,
|
||||
oel.herstellerMonate,
|
||||
oel.km,
|
||||
oel.monate,
|
||||
fahrten,
|
||||
jetzt,
|
||||
)
|
||||
return eigen ?? roh()
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ Nächster Service */
|
||||
@@ -219,8 +281,8 @@ export function naechsterService(
|
||||
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 m = oelMeldungsPrognose(fahrzeug, fahrten, jetzt)
|
||||
if (m) kandidaten.push({ art: "Ölwechsel", restKm: m.restKm, datum: m.datum })
|
||||
}
|
||||
|
||||
const insp = inspektionPrognose(fahrzeug, buch, jetzt)
|
||||
@@ -228,7 +290,7 @@ export function naechsterService(
|
||||
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 })
|
||||
if (m) kandidaten.push({ art: "Inspektion", restKm: m.restKm, datum: m.datum })
|
||||
}
|
||||
|
||||
// fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.
|
||||
|
||||
Reference in New Issue
Block a user