Ein Update-Knopf, Modell als Auswahlfeld, abgestellt schliesst fahrend aus

- Die Update-Kachel hat genau einen Knopf am Ende. Er traegt in jedem
  Zustand den naechsten faelligen Schritt (suchen, installieren, Neustart,
  Neuladen); Beschriftung und Fuellung wechseln mit. Die Abschnitte
  darueber behalten ihre Werte und verlieren nur ihre Knoepfe.
- Abstand unter der Trennlinie von 28 auf 16 px: das padding-top verhindert
  das Kollabieren des margin der Werteliste, also zaehlt jetzt nur noch das
  padding.
- Modell ist in der App ein Auswahlfeld statt eines Textfelds, wie im
  Panel. Die Liste liegt als Zweitschrift in daten/fahrzeugmodelle.ts;
  fahrzeugmodelle.test.ts liest die Panel-Quelle ein und vergleicht sie
  Eintrag fuer Eintrag, damit die beiden nicht auseinanderlaufen.
- Faehrt der Wagen, sagen Uebersicht und Sicherheitsseite das, statt
  "sicher abgestellt" zu behaupten. Die Zuendung meldet der Dongle in
  Sekunden, Tuer- und Schlossmeldungen kommen verzoegert - die schnellere
  Quelle hat Vorrang, weil sonst das Wort nicht stimmt.
- tankerkennung: der Tiefststand wird vor dem Anlegen gesetzt. Ein
  gepufferter Schwung Datensaetze legte sonst denselben Tankvorgang
  zweimal an, weil der zweite Zustandswechsel waehrend des Wartens noch
  den alten Tiefststand las (04.09.2026, 29,8 l und 29,7 l).
- flespi: nach dem Leeren des Zwischenspeichers wird nur abgeglichen, wenn
  ein Geraetewert vorliegt. Sonst ueberschrieb der Waechterfehler den
  frisch gelesenen Stand in der Kachel.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-05 17:12:51 +02:00
co-authored by Claude Opus 5
parent 1838ab0401
commit 255a593b50
14 changed files with 469 additions and 73 deletions
@@ -0,0 +1,42 @@
/**
* Die Modellliste der App muss der des Panels entsprechen.
*
* Bis zum 05.09.2026 war Modell in der App ein freies Textfeld, im Panel ein
* Auswahlfeld - vom Eigentümer bemerkt. Beim Nachziehen entstand eine
* Zweitschrift der Liste; dieser Test schneidet die Panel-Fassung aus der
* Quelle heraus und vergleicht sie, damit die beiden nicht auseinanderlaufen.
*/
import { readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { FAHRZEUG_MODELLE, modellauswahl } from "./fahrzeugmodelle"
function panelListe(): string[] {
const quelle = readFileSync(
"../custom_components/audi_dashboard/frontend/audi-dashboard-app.js",
"utf8",
)
const start = quelle.indexOf("const FAHRZEUG_MODELLE = [")
if (start < 0) throw new Error("FAHRZEUG_MODELLE nicht in der Panel-Quelle gefunden")
const ende = quelle.indexOf("];", start)
const roh = quelle.slice(start, ende)
return [...roh.matchAll(/"([^"]+)"/g)].map((m) => m[1] as string)
}
describe("Modellliste", () => {
it("stimmt mit der des Panels überein", () => {
expect([...FAHRZEUG_MODELLE]).toEqual(panelListe())
})
it("stellt einen unbekannten gespeicherten Titel voran", () => {
const auswahl = modellauswahl("RS 6 Avant performance")
expect(auswahl[0]).toBe("RS 6 Avant performance")
expect(auswahl).toHaveLength(FAHRZEUG_MODELLE.length + 1)
})
it("laesst die Liste unveraendert, wenn der Titel darin steht", () => {
expect(modellauswahl("RS 4 Avant")).toEqual(FAHRZEUG_MODELLE)
})
})
@@ -0,0 +1,38 @@
/**
* Die Modelle, zu denen ein echtes Badge vorliegt.
*
* Zweitschrift von `FAHRZEUG_MODELLE` im Panel
* (custom_components/audi_dashboard/frontend/audi-dashboard-app.js). Zwei
* Codebasen, eine Liste - dass sie gleich bleiben, sichert
* `fahrzeugmodelle.test.ts`, der die Panel-Quelle einliest und Eintrag für
* Eintrag vergleicht.
*
* Beschränkt auf Modelle mit Badge: für alles andere stünde ein falsches oder
* gar kein Badge auf der Übersicht. Die Ausführung („competition",
* „performance") gehört nicht hierher, sondern in das Feld „Ausführung" -
* Entscheidung des Eigentümers vom 05.09.2026.
*/
export const FAHRZEUG_MODELLE = [
"RS 3 Sportback",
"RS 3 Limousine",
"RS 4 Avant",
"RS 5 Coupé",
"RS 5 Sportback",
"RS 6 Avant",
"RS 7 Sportback",
"S3 Sportback",
"S3 Limousine",
"SQ7",
] as const
/** Die Liste, wie sie im Auswahlfeld stehen muss.
*
* Ein gespeicherter Titel, der nicht (mehr) in der Liste steht, wird
* vorangestellt statt verschluckt - sonst fiele die Auswahl still auf den
* ersten Eintrag zurück und änderte das Fahrzeug des Nutzers. Wortgleich mit
* dem Panel. */
export function modellauswahl(titel: string): readonly string[] {
return FAHRZEUG_MODELLE.includes(titel as (typeof FAHRZEUG_MODELLE)[number])
? FAHRZEUG_MODELLE
: [titel, ...FAHRZEUG_MODELLE]
}
+75 -43
View File
@@ -13,6 +13,7 @@ import type { LetzterDatensatz, Profil } from "../api"
import { eigeneVersion, versionOrdnung } from "../daten/appVersion"
import { Datumsfeld } from "./Datumsfeld"
import { useDaten } from "../daten/DatenKontext"
import { modellauswahl } from "../daten/fahrzeugmodelle"
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
import { datum, datumZeit, de, isoTag, standAlter } from "../format"
@@ -403,13 +404,28 @@ export function Einstellungen({
<span className="ads-eyebrow">Fahrzeug einrichten</span>
{einrichtenOffen && (
<>
{/* Auswahlfeld, nicht Freitext - wie im Panel (<select id="einModell">).
Bis zum 05.09.2026 stand hier ein einfaches Eingabefeld, vom
Eigentümer bemerkt: „einrichten/modell ist in der app kein
dropdown". Ein frei getippter Titel führt zu keinem oder zum
falschen Typenschild, denn das Bild wird über den Modellnamen
gesucht. Die Liste selbst steht in daten/fahrzeugmodelle.ts und
wird von einem Test gegen die Panel-Fassung geprüft. */}
<Feld label="Modell">
<input
<select
className="dm-eingabe dm-eingabe--fahrzeug"
value={werte.fahrzeugtitel}
onChange={(e) => aendern({ fahrzeugtitel: e.target.value })}
onBlur={() => sichern()}
/>
onChange={(e) => {
aendern({ fahrzeugtitel: e.target.value })
sichern({ fahrzeugtitel: e.target.value })
}}
>
{modellauswahl(werte.fahrzeugtitel).map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</Feld>
<Feld label="Kennzeichen">
<input
@@ -782,20 +798,10 @@ export function Einstellungen({
zum nächsten nötigen Schritt (Neustart) statt den Nutzer selbst
suchen zu lassen - ein reiner Reload reicht nicht (AGENTS.md
Abschnitt J). */}
{updateInstallierenLaeuft ? (
<div className="dm-ladezeile">
<span className="dm-spinner" aria-hidden="true" />
<span className="dm-erklaerung" style={{ marginTop: 0 }}>
Installiere
</span>
</div>
) : updateNeustartLaeuft ? (
<div className="dm-ladezeile">
<span className="dm-spinner" aria-hidden="true" />
<span className="dm-erklaerung" style={{ marginTop: 0 }}>
Home Assistant startet neu
</span>
</div>
{updateInstallierenLaeuft || updateNeustartLaeuft ? (
/* Kein eigener Text mehr - was gerade läuft, steht im Knopf ganz
unten, der dabei selbst den Fortschritt zeigt. */
<></>
) : integrationUpdate?.installiert && integrationUpdate.neustart_noetig === false ? (
/* Gar keine Meldung. Geändert wurden Dateien unter frontend/, und das
sind die des PANELS - die App lädt ihren eigenen Code aus
@@ -813,11 +819,6 @@ export function Einstellungen({
Version {integrationUpdate.version} installiert - die neue Fassung wird erst nach
einem Neustart geladen.
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void updateNeustartAusloesen()}>
Installation abschließen und neu starten
</ActionButton>
</div>
</>
) : integrationUpdate?.verfuegbar ? (
<>
@@ -840,11 +841,6 @@ export function Einstellungen({
/>
}
/>
<div className="dm-knopfreihe">
<ActionButton variant="primary" onClick={() => void updateInstallierenAusloesen()}>
Update installieren
</ActionButton>
</div>
</>
) : (
<>
@@ -877,11 +873,6 @@ export function Einstellungen({
Lädt die Oberfläche neu und startet die App neu. Fahrzeugprofil, Fahrten und
Tankvorgänge sind davon nicht betroffen die liegen in Home Assistant.
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void otaAusloesen()} disabled={otaLaeuft}>
{otaLaeuft ? "Lade Update …" : "Update installieren"}
</ActionButton>
</div>
{otaFehler && <p className="dm-fehler">{otaFehler}</p>}
</>
) : otaBuendel &&
@@ -912,18 +903,59 @@ export function Einstellungen({
</div>
)}
{/* Ein Suchknopf für die ganze Kachel, ganz unten: er steht damit unter
beiden Abschnitten und legt nahe, was er tut - nach Neuem für
Integration UND App sehen (Vorgabe des Eigentümers, 05.09.2026).
{/* EIN Knopf für die ganze Kachel, ganz unten: er steht unter beiden
Abschnitten und trägt in jedem Zustand den nächsten fälligen
Schritt - suchen, installieren, Neustart. Bis zum 05.09.2026 stand
darüber noch ein zweiter Knopf im jeweiligen Abschnitt; der
Eigentümer: „warum nicht den update suchen button dafür verwenden
und nur die optik und schrift zur installation anpassen?" - genau
das passiert hier, Beschriftung und Füllung wechseln mit dem
Zustand. Wortgleich mit dem Panel (updateKnopf()).
Er ist in JEDEM Zustand da. Vorher sass er nur im Zweig "ist
aktuell", und daneben brauchte es ein zweites "Erneut prüfen", damit
man eine womöglich wochenalte Zahl überhaupt auffrischen konnte,
solange ein Update angeboten wurde. Das erübrigt sich hier. */}
Die Reihenfolge ist eine Rangfolge, keine Auswahl: liegt ein
Neustart an, ist das der nächste Schritt, egal was sonst noch
bereitliegt. Integrations- UND App-Update gleichzeitig gibt es
dabei praktisch nicht - das Bündel wird erst angeboten, wenn es zur
laufenden Serverfassung passt (buendelPasst), also nach der
Integration. */}
<div className="dm-knopfreihe" style={{ marginTop: "var(--sp-4)" }}>
<ActionButton onClick={() => void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}>
{updatePruefenLaeuft ? "Prüfe …" : "Auf Update prüfen"}
</ActionButton>
{(() => {
const laufend = (text: string) => (
<ActionButton disabled>
<span className="dm-knopflader">
<span className="dm-spinner" aria-hidden="true" />
{text}
</span>
</ActionButton>
)
if (updateInstallierenLaeuft) return laufend("Installiere …")
if (updateNeustartLaeuft) return laufend("Home Assistant startet neu …")
if (updatePruefenLaeuft) return laufend("Prüfe …")
if (otaLaeuft) return laufend("Lade Update …")
if (integrationUpdate?.installiert && integrationUpdate.neustart_noetig !== false)
return (
<ActionButton variant="primary" onClick={() => void updateNeustartAusloesen()}>
Installation abschließen und neu starten
</ActionButton>
)
if (integrationUpdate?.verfuegbar)
return (
<ActionButton variant="primary" onClick={() => void updateInstallierenAusloesen()}>
Update installieren
</ActionButton>
)
if (otaUpdateVerfuegbar && otaBuendel)
return (
<ActionButton variant="primary" onClick={() => void otaAusloesen()}>
Update installieren
</ActionButton>
)
return (
<ActionButton onClick={() => void updatePruefenAusloesen()}>
Auf Update prüfen
</ActionButton>
)
})()}
</div>
{/* Der Zeitpunkt gehört zum Suchvorgang, nicht zu einem der beiden
Abschnitte - er stand bis zum 05.09.2026 an der grünen Marke der
+14 -3
View File
@@ -17,6 +17,7 @@ import { useDaten } from "../daten/DatenKontext"
import type { SeitenName } from "../navigation"
import { gruppenErgebnis, Leerzustand, StatusKreis } from "./bausteine"
import { Bild } from "./Bild"
import { standortZustand } from "./Standort"
export function Fahrzeugstatus({ geheZu }: { geheZu: (name: SeitenName) => void }) {
const { fahrzeug } = useDaten()
@@ -37,8 +38,15 @@ export function Fahrzeugstatus({ geheZu }: { geheZu: (name: SeitenName) => void
const fensterDach = gruppenErgebnis(punkte.filter((p) => p.gruppe === "fenster_dach"))
const licht = gruppenErgebnis(punkte.filter((p) => p.gruppe === "licht"))
const gesichert = fahrzeug.gesichert
const sammelText =
gesichert === null
/* Dieselbe Regel wie auf der Übersicht: unterwegs ist der Wagen nicht
abgestellt (Vorgabe des Eigentümers, 05.09.2026). Die vier Sammelzeilen
darunter bleiben stehen und zeigen weiter, was die Tür-, Fenster- und
Schlosssensoren melden — nur die Überschrift darf nicht „abgestellt"
behaupten, während die Zündung an ist. */
const faehrt = standortZustand(fahrzeug).faehrt
const sammelText = faehrt
? "Zündung an - Fahrzeug in Betrieb"
: gesichert === null
? "Zustand nicht vollständig bekannt"
: gesichert
? "Fahrzeug ist sicher abgestellt"
@@ -57,7 +65,10 @@ export function Fahrzeugstatus({ geheZu }: { geheZu: (name: SeitenName) => void
Kreis-Häkchen-Symbol wie die vier Zeilen unten (nicht der generische
StatusRow-Punkt), keine Zeitangabe - das Panel zeigt dort keine. */}
<Tile className="dm-sicherheitssammel">
<StatusKreis ok={gesichert} />
{/* Während der Fahrt weder Haken noch Kreuz noch Fragezeichen, sondern
derselbe Punkt wie im Standortmenü — der Zustand ist dann nicht
„geprüft", sondern schlicht ein anderer. */}
{faehrt ? <span className="ads-dot" /> : <StatusKreis ok={gesichert} />}
<span>{sammelText}</span>
</Tile>
+1 -1
View File
@@ -124,7 +124,7 @@ const PARKPLATZ_AB_MIN = 15
Kein Rueckfall auf die Fahrtenliste: null heisst unbekannt, und das wird
auch so angezeigt. Eine falsche Zahl ist schlechter als keine. */
function standortZustand(
export function standortZustand(
fahrzeug: Fahrzeug,
): { faehrt: boolean; unbekannt: boolean; seitTs: number | null; geparkt: boolean } {
const roh = fahrzeug.geparktSeit ? new Date(fahrzeug.geparktSeit).getTime() : NaN
+27 -7
View File
@@ -21,7 +21,7 @@ import { Blattzeile, Leerzustand } from "./bausteine"
import { strecke } from "./Fahrten"
import { Bild } from "./Bild"
import type { ServicebuchEintrag } from "./Service"
import { StandortVorschau } from "./Standort"
import { StandortVorschau, standortZustand } from "./Standort"
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
import { Typenschild, schildErkennen } from "./Typenschild"
@@ -87,12 +87,32 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
.slice()
.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0]
const sicherheitsstatus =
fahrzeug.gesichert === true ? "ok" : fahrzeug.gesichert === false ? "bad" : "warn"
// Wortlaut deckungsgleich mit vHome() im Panel (g === null ? "Status unbekannt" :
// g ? "Sicher abgestellt" : "Nicht sicher abgestellt").
const sicherheitstext =
fahrzeug.gesichert === true
/* Fährt der Wagen, kann er nicht gleichzeitig „sicher abgestellt" sein —
abgestellt und unterwegs schließen einander aus (Vorgabe des Eigentümers,
05.09.2026: „es kann nicht sein dass das fahrzeug sicher abgestellt ist und
zeitgleich fährt").
Die beiden Aussagen kommen aus verschiedenen Quellen: Tür-, Fenster- und
Schlossmeldungen liefert der EU Data Act mit spürbarer Verzögerung, die
Zündung meldet der Dongle in Sekunden. Die schnellere Quelle hat hier
Vorrang — nicht weil die andere falsch wäre (verriegelt ist der Wagen
während der Fahrt tatsächlich), sondern weil das WORT „abgestellt" dann
nicht stimmt.
Wortlaut wie im Standortmenü: „Zündung an", nicht „fährt" — einen
Bewegungssensor gibt es nicht, die Zündung steht auch im Stand mit
laufendem Motor auf an. Deckungsgleich mit vHome() im Panel. */
const faehrt = standortZustand(fahrzeug).faehrt
const sicherheitsstatus = faehrt
? "ok"
: fahrzeug.gesichert === true
? "ok"
: fahrzeug.gesichert === false
? "bad"
: "warn"
const sicherheitstext = faehrt
? "Zündung an"
: fahrzeug.gesichert === true
? "Sicher abgestellt"
: fahrzeug.gesichert === false
? "Nicht sicher abgestellt"
+20
View File
@@ -1700,6 +1700,16 @@ button.dm-listenzeile {
/* Die Ueberschrift eines Abschnitts ist kein direktes Kind der Kachel, die
Regel .ads-tile > .ads-eyebrow:first-child greift hier also nicht. */
/* Der Strich zwischen zwei Abschnitten und die erste Zeile darunter standen
28px auseinander: 16px padding-top der Regel oben PLUS die 12px margin-top
der Werteliste - die kollabieren wegen des padding nicht miteinander. Vom
Eigentümer bemerkt (05.09.2026: „abstand zwischen app und version und dem
darüberliegendem strich ist zu groß"). Jetzt zählt nur noch das padding,
in beiden Abschnitten gleich. */
.dm-kachelabschnitt > :first-child {
margin-top: 0;
}
.dm-kachelabschnitt > .ads-eyebrow:first-child {
display: block;
margin-bottom: 14px;
@@ -2363,3 +2373,13 @@ select.dm-auswahl {
input[type="range"] {
min-height: 44px;
}
/* Spinner UND Beschriftung in EINEM Knopf: seit dem 05.09.2026 trägt der
Knopf der Update-Kachel den laufenden Vorgang selbst, statt dass daneben
eine eigene Ladezeile steht (Vorgabe des Eigentümers: „warum nicht den
update suchen button dafür verwenden"). */
.dm-knopflader {
display: inline-flex;
align-items: center;
gap: 10px;
}