Debug-Mode, eine Version-Kachel, Knoepfe nach HIG, Bilder ohne Verzoegerung (2026.9.4.11)
Debug-Mode: Zugaenge und Dongle stehen nicht mehr verstreut oben in den Einstellungen, sondern zusammen hinter einem Schalter am Seitenende - dazu neu der letzte Datensatz des Dongles (geraet.py). "Letzter Datensatz" heisst dabei: alle Entitaeten des Geraets, deren Aktualisierung hoechstens zwei Sekunden nach der juengsten liegt - die flespi-Integration setzt einen Datensatz in Millisekunden. Angezeigt mit Ankunft, Geraetezeit und dem Rueckstand zwischen beiden. Beim Verifizieren gefunden: app_version_veroeffentlichen() lief nur beim Start und nach einer Aktion, und beim Start haben die Entitaeten des Geraets noch keinen Zustand - die Kachel waere dauerhaft leer geblieben. Laeuft jetzt im Minutentakt mit, ohne Netzaufruf. Ausserdem: - "Version" und "Integration-Update" sind eine Kachel - Speichern/Verwerfen als zwei gleich breite Knoepfe, der bestaetigende gefuellt und rechts; "Verwerfen" war in der App blosser Text - Bilder werden nach dem ersten Zeichnen in der Leerlaufzeit vorgeladen UND dekodiert. Die Verzoegerung beim Seitenwechsel kam nicht vom Netz (31 Tage Cache), sondern vom asynchronen Dekodieren eines frisch erzeugten <img>. 214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
This commit is contained in:
@@ -6,8 +6,9 @@ Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als d
|
||||
dazu die Kopfmarke statt der Firmierung des Betreibers und ein
|
||||
Cache-Brecher auf der .ipa-Adresse; die .ipa liess sich zuletzt nicht mehr
|
||||
installieren - der Erweiterung fehlte die CFBundleVersion; dazu der QR-Scanner
|
||||
fuer den Zugangs-Token, `2026.9.4.8`,
|
||||
Abschnitte CC bis CE. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
fuer den Zugangs-Token; dazu Debug-Mode, eine Version-Kachel, das Knopfpaar nach
|
||||
HIG und Bilder ohne Verzoegerung, `2026.9.4.11`,
|
||||
Abschnitte CC bis CF. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
|
||||
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
|
||||
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
|
||||
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
|
||||
@@ -9844,3 +9845,100 @@ unter „Zugangs-Token", das Blatt oeffnet mit Videofeld und Zielrahmen, und ohn
|
||||
Kamerafreigabe steht dort die erklaerende Meldung statt einer leeren Flaeche.
|
||||
**Nicht geprueft, weil es in dieser Umgebung nicht geht:** ein echter Scan - die
|
||||
Browser-Vorschau gibt keine Kamera heraus. Das zeigt sich erst auf dem Geraet.
|
||||
|
||||
## CF. Debug-Mode, eine Version-Kachel, Knoepfe nach HIG, Bilder ohne Verzoegerung (2026.9.4.11)
|
||||
|
||||
Vier Punkte aus demselben Stapel des Eigentuemers, beide Codebasen.
|
||||
|
||||
### Debug-Mode: Zugaenge, Dongle und der letzte Datensatz, zugeklappt und unten
|
||||
|
||||
„Zugaenge" stand bisher als zweite Kachel ganz oben, „Dongle" mitten in der
|
||||
Seite - beides Dinge, die man einmal im Vierteljahr anfasst. Sie stehen jetzt
|
||||
zusammen hinter einem Schalter am Seitenende, zusammen mit einer dritten,
|
||||
neuen Kachel.
|
||||
|
||||
**`geraet.py` (neu): der letzte Datensatz des Dongles.** Die flespi-Integration
|
||||
setzt die Entitaeten EINES Geraetedatensatzes nacheinander, aber innerhalb
|
||||
weniger Millisekunden (am 01.09.2026 gemessen: 2 ms zwischen Zuendung und
|
||||
zugehoeriger Meldezeit, Abschnitt AY). „Der letzte Datensatz" ist deshalb
|
||||
definiert als: alle Entitaeten des Geraets, deren letzte Aktualisierung
|
||||
hoechstens `FENSTER_S` (2 s) nach der juengsten liegt. Das Geraet wird nicht
|
||||
geraten, sondern ueber das Entitaetsregister der zugeordneten Meldezeit
|
||||
bestimmt (sonst Fahrtsignal, sonst Zuendung); findet sich keines, bleibt der
|
||||
Wert `None`.
|
||||
|
||||
Angezeigt werden Geraetename, **Ankunft**, **Geraetezeit** und der **Rueckstand**
|
||||
zwischen beiden - genau die Angabe, deren Missachtung die Wurzel des
|
||||
3-km-Fahrt-Fehlers war (Abschnitt CB).
|
||||
|
||||
**Ein echter Fund beim Verifizieren:** die Kachel blieb leer.
|
||||
`app_version_veroeffentlichen()` laeuft naemlich **nur** beim Start und nach
|
||||
einer Aktion (Token, Dongle, Update) - beim Start haben die Entitaeten des
|
||||
Geraets aber noch gar keinen Zustand, und danach wurde nie wieder
|
||||
veroeffentlicht. Der Minutentakt ruft sie jetzt mit. Kosten: ein kleiner
|
||||
Dateilesevorgang (`bundle.json`) je Minute, **kein** Netzaufruf - die Pruefung
|
||||
bei Gitea bleibt an ihrem Dienst (Abschnitt J).
|
||||
|
||||
Live nachgewiesen im Panel: 46 Werte aus derselben Meldung, Geraetezeit
|
||||
04.09. 00:49 gegen Ankunft 12:08 - Rueckstand 40.745 s, weil der Dongle seit
|
||||
dem Vorabend schweigt und „Angekommen" nach einem Neustart Home Assistants
|
||||
eigener Zeitpunkt ist. Genau das sagt die Fussnote der Kachel auch.
|
||||
|
||||
### „Version" und „Integration-Update" sind eine Kachel
|
||||
|
||||
Es ist dieselbe Sache aus zwei Blickwinkeln; zwei Kacheln nebeneinander
|
||||
zwangen dazu, zwei Zahlen ueber eine Kachelgrenze hinweg zu vergleichen.
|
||||
|
||||
### Das Knopfpaar Speichern/Verwerfen
|
||||
|
||||
Gemeldet: „Verwerfen ist kein richtiger Button". Stimmt - in der App war es
|
||||
ein blosser Textknopf neben einem vollen. Jetzt **zwei gleich breite Knoepfe**,
|
||||
der bestaetigende gefuellt und rechts, der Ausweg daneben als zweite Rolle:
|
||||
ein Paar aus Bestaetigen und Abbrechen ist eine Entscheidung zwischen zwei
|
||||
Wegen, keine Haupt- und Nebensache, und welcher gewollt ist, sagt die Fuellung
|
||||
und nicht die Groesse.
|
||||
|
||||
Geaendert an **beiden** Stellen der App (Regler-Kachel und Speicherleiste am
|
||||
Seitenende), sonst haette dieselbe Sache zwei Muster auf einer Seite. Im Panel
|
||||
standen beide bereits als volle Knoepfe, nur untereinander - dort jetzt
|
||||
dieselbe Zeile (`.knopfpaar`).
|
||||
|
||||
**Nicht angefasst:** die „Abbrechen"-Textknoepfe der Bearbeiten-Formulare in
|
||||
`FahrtDetail.tsx`/`TankDetail.tsx`. Dieselbe Frage, aber nicht gemeldet und ein
|
||||
anderer Zusammenhang (Formular statt Einstellung) - hier vermerkt, nicht still
|
||||
mitgeaendert.
|
||||
|
||||
### Bilder erscheinen ohne Verzoegerung
|
||||
|
||||
Gemeldet: „Bilder laden beim Umschalten der Menues einige ms verzoegert."
|
||||
|
||||
**Am Netz liegt es nicht**, und das ist der Punkt: die Adresse ist innerhalb
|
||||
einer Sitzung stabil, und Home Assistant liefert `/local/` mit 31 Tagen
|
||||
Cache-Dauer (in Abschnitt AR gemessen). Verzoegert wird das Bild selbst - ein
|
||||
frisch erzeugtes `<img>` wird standardmaessig **asynchron dekodiert** und erst
|
||||
danach gezeichnet, und beim Seitenwechsel entsteht jedes Mal ein neues.
|
||||
|
||||
`bilderVorladen()` holt alle Fahrzeugfotos einmal und ruft `decode()` darauf.
|
||||
Danach zeichnet dasselbe Bild beim naechsten Einhaengen sofort. Ausgefuehrt in
|
||||
der **Leerlaufzeit** nach dem ersten Zeichnen (`requestIdleCallback`, sonst
|
||||
`setTimeout`), damit es nicht mit dem Abruf von Profil, Fahrten und
|
||||
Tankvorgaengen konkurriert; das Datenvolumen ist dasselbe wie beim
|
||||
Durchblaettern aller Seiten, nur frueher.
|
||||
|
||||
Live nachgewiesen im Panel: nach dem Laden stehen alle neun Bildplaetze plus
|
||||
die Winter-Seitenansicht in `performance.getEntriesByType("resource")`.
|
||||
|
||||
### Verifiziert
|
||||
|
||||
`py_compile` auf `geraet.py` und `koordinator.py`, Panel als **Modul** geparst,
|
||||
`tsc --noEmit` sauber, **214/214** Tests, `vite build` sauber, `audi_ha_test`
|
||||
auf `2026.9.4.11` sauber gestartet, und im Panel live nachgesehen: Debug-Mode
|
||||
klappt auf und zeigt Zugaenge, Dongle und die 49 Zeilen des letzten
|
||||
Datensatzes; „Version" traegt jetzt Installiert **und** Update.
|
||||
|
||||
**Nicht live geprueft:** dieselben Kacheln in der App. Die Browser-Vorschau
|
||||
kennt keinen gespeicherten Zugang, und einen Token anzulegen oder einzugeben
|
||||
ist ausgeschlossen. Abgedeckt sind sie durch `tsc`, den
|
||||
Alle-Seiten-Rendertest und die Tatsache, dass das Panel dieselbe Struktur aus
|
||||
denselben Backend-Daten baut - der Zweig MIT Datensatz ist in der App damit
|
||||
strukturell, nicht optisch belegt.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { bilderVorladen } from "./screens/bilder"
|
||||
|
||||
import { App as CapacitorApp } from "@capacitor/app"
|
||||
|
||||
import { DataMetricApi, zugangLesen } from "./api"
|
||||
@@ -79,6 +81,13 @@ function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) {
|
||||
void startklarMelden()
|
||||
}, [])
|
||||
|
||||
/* Fahrzeugfotos einmal vorab holen und dekodieren, sobald Daten da sind -
|
||||
siehe bilderVorladen(). Damit erscheint beim Umschalten der Seiten sofort
|
||||
das Bild statt kurz des Platzhalters. */
|
||||
useEffect(() => {
|
||||
if (daten.verbindung === "verbunden") bilderVorladen()
|
||||
}, [daten.verbindung])
|
||||
|
||||
/* Belege aus dem Teilen-Blatt.
|
||||
|
||||
Die Erweiterung legt das PDF ab und oeffnet die App - beim ersten Start
|
||||
|
||||
@@ -271,6 +271,24 @@ export interface DongleAngabe {
|
||||
stand: DongleStand | null;
|
||||
}
|
||||
|
||||
/** Ein einzelner Wert aus der letzten Meldung des Dongles (geraet.py). */
|
||||
export interface DatensatzWert {
|
||||
name: string;
|
||||
wert: string;
|
||||
einheit: string | null;
|
||||
}
|
||||
|
||||
/** Die letzte Meldung des Dongles, so wie sie in Home Assistant ankam.
|
||||
"angekommen" ist die Ankunftszeit, "meldezeit" die Zeit des Geraets -
|
||||
der Abstand zwischen beiden ist der Puffer-Rueckstand. */
|
||||
export interface LetzterDatensatz {
|
||||
geraet: string;
|
||||
angekommen: string;
|
||||
meldezeit: string | null;
|
||||
werte: DatensatzWert[];
|
||||
gesamt: number;
|
||||
}
|
||||
|
||||
/** Nutzlast von sensor.audi_dashboard_app_version. */
|
||||
export interface AppVersionAngabe {
|
||||
app: string | null;
|
||||
@@ -278,6 +296,7 @@ export interface AppVersionAngabe {
|
||||
integration_update: IntegrationUpdateAngabe | null;
|
||||
zugaenge?: ZugangAngabe[];
|
||||
dongle?: DongleAngabe;
|
||||
letzter_datensatz?: LetzterDatensatz | null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- Entitäts-Verzeichnis */
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type Fahrzeugstatus,
|
||||
type IntegrationUpdateAngabe,
|
||||
type DongleAngabe,
|
||||
type LetzterDatensatz,
|
||||
type ZugangAngabe,
|
||||
type Profil,
|
||||
type Tankvorgang,
|
||||
@@ -75,6 +76,7 @@ export interface DatenWert {
|
||||
/** Konfiguration des Dongles und eine ggf. vorgemerkte Aenderung
|
||||
(flespi.py). null, solange kein flespi-Token hinterlegt ist. */
|
||||
dongle: DongleAngabe | null
|
||||
letzterDatensatz: LetzterDatensatz | null
|
||||
|
||||
einstellungen: Einstellungen | null
|
||||
fahrzeug: Fahrzeug | null
|
||||
@@ -123,6 +125,7 @@ export function DatenAnbieter({
|
||||
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
|
||||
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
|
||||
const [dongle, setzeDongle] = useState<DongleAngabe | null>(null)
|
||||
const [letzterDatensatz, setzeLetzterDatensatz] = useState<LetzterDatensatz | null>(null)
|
||||
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
|
||||
null,
|
||||
)
|
||||
@@ -168,6 +171,7 @@ export function DatenAnbieter({
|
||||
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
|
||||
setzeZugaenge(versionsangabe?.zugaenge ?? [])
|
||||
setzeDongle(versionsangabe?.dongle ?? null)
|
||||
setzeLetzterDatensatz(versionsangabe?.letzter_datensatz ?? null)
|
||||
return true
|
||||
} catch (fehler) {
|
||||
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
|
||||
@@ -301,6 +305,7 @@ export function DatenAnbieter({
|
||||
integrationUpdate,
|
||||
zugaenge,
|
||||
dongle,
|
||||
letzterDatensatz,
|
||||
einstellungen,
|
||||
fahrzeug,
|
||||
fahrten,
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useEffect, useRef, useState } from "react"
|
||||
import { Accordion, ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { ApiFehler, DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api"
|
||||
import type { Profil } from "../api"
|
||||
import type { LetzterDatensatz, Profil } from "../api"
|
||||
import { eigeneVersion } from "../daten/appVersion"
|
||||
import { Datumsfeld } from "./Datumsfeld"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
|
||||
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
|
||||
import { datum, datumZeit, isoTag, standAlter } from "../format"
|
||||
import { datum, datumZeit, de, isoTag, standAlter } from "../format"
|
||||
import { useTheme } from "../theme"
|
||||
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
|
||||
@@ -44,6 +44,7 @@ export function Einstellungen({
|
||||
serverVersion,
|
||||
zugaenge,
|
||||
dongle,
|
||||
letzterDatensatz,
|
||||
verbindung,
|
||||
api,
|
||||
profilSpeichern,
|
||||
@@ -250,6 +251,7 @@ export function Einstellungen({
|
||||
// Geraet) und darf nicht wie ein toter Knopf aussehen. Das Ergebnis selbst
|
||||
// kommt ueber die veroeffentlichte Entitaet zurueck (flespi.py), hier steht
|
||||
// nur, was den Aufruf gar nicht erst erreicht hat.
|
||||
const [debugOffen, setzeDebugOffen] = useState(false)
|
||||
const [dongleLaeuft, setzeDongleLaeuft] = useState(false)
|
||||
const [dongleFehler, setzeDongleFehler] = useState<string | null>(null)
|
||||
|
||||
@@ -409,66 +411,6 @@ export function Einstellungen({
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{zugaenge.length > 0 && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Zugänge</span>
|
||||
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben —
|
||||
man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */}
|
||||
{zugaenge.map((z) => (
|
||||
<Accordion
|
||||
key={z.dienst}
|
||||
title={z.titel}
|
||||
summary={
|
||||
<span className={z.gesetzt ? "dm-token-gesetzt" : "dm-token-offen"}>
|
||||
{z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p className="dm-erklaerung">{z.zweck}</p>
|
||||
<Feld label="Neuer Token" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}
|
||||
value={tokenEntwurf[z.dienst] ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
disabled={tokenLaeuft !== null || !(tokenEntwurf[z.dienst] ?? "").trim()}
|
||||
onClick={() => void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")}
|
||||
>
|
||||
{tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"}
|
||||
</ActionButton>
|
||||
{z.gesetzt && (
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen("Token löschen?")) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
{tokenFehler && <p className="dm-fehler">{tokenFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist.
|
||||
Dieselben Felder gibt es unter Einstellungen → Geräte &
|
||||
Dienste → Audi Dashboard → Konfigurieren.
|
||||
</span>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Fahrt beenden</span>
|
||||
<Slider
|
||||
@@ -482,24 +424,24 @@ export function Einstellungen({
|
||||
/>
|
||||
{/* Erscheint erst, wenn der Regler wirklich woanders steht - ein
|
||||
dauerhaft sichtbarer, meist wirkungsloser Knopf waere Beiwerk. Die
|
||||
Anordnung folgt der Speicherleiste am Seitenende: bestaetigende
|
||||
Aktion als voller Knopf, der Ausweg daneben als schlichter Text. */}
|
||||
Anordnung folgt Apples eigener Aufteilung fuer ein Wertepaar aus
|
||||
Bestaetigen und Abbrechen: ZWEI gleich grosse Knoepfe, der
|
||||
bestaetigende gefuellt und rechts (die fuehrende Hand erreicht ihn
|
||||
zuerst), der Ausweg daneben als zweite Rolle. Vorher war "Verwerfen"
|
||||
blosser Text und dadurch kaum als Bedienelement zu erkennen -
|
||||
gemeldet vom Eigentuemer am 04.09.2026. */}
|
||||
{pausenzeitGeaendert && (
|
||||
<div className="dm-speicherleiste dm-speicherleiste--kachel">
|
||||
<ActionButton onClick={() => setzePausenzeitEntwurf(null)} disabled={laeuft}>
|
||||
Verwerfen
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
onClick={() => void speichern({ pausenzeitMin: pausenzeitEntwurf as number })}
|
||||
disabled={laeuft}
|
||||
>
|
||||
{laeuft ? "Speichere …" : "Speichern"}
|
||||
</ActionButton>
|
||||
<button
|
||||
type="button"
|
||||
className="dm-textknopf"
|
||||
onClick={() => setzePausenzeitEntwurf(null)}
|
||||
disabled={laeuft}
|
||||
>
|
||||
Verwerfen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
||||
@@ -513,44 +455,6 @@ export function Einstellungen({
|
||||
</span>
|
||||
</Tile>
|
||||
|
||||
{dongle?.aktiv && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Dongle</span>
|
||||
{dongle.stand?.fehler && <p className="dm-fehler">{dongle.stand.fehler}</p>}
|
||||
{(dongle.stand?.werte ?? []).length > 0 && (
|
||||
<Werteliste
|
||||
kinder={(dongle.stand?.werte ?? []).map((w) => (
|
||||
<Wertzeile
|
||||
key={w.schluessel}
|
||||
label={`${w.titel} ${w.nummer}`}
|
||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||
warnung={Boolean(w.abweichung)}
|
||||
zusatz={
|
||||
w.offen != null
|
||||
? `→ ${w.offen}, unterwegs`
|
||||
: w.abweichung
|
||||
? `(wir: ${w.unser})`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
<ActionButton disabled={dongleLaeuft} onClick={() => void dongleAbrufen()}>
|
||||
{dongleLaeuft ? "Liest …" : "Jetzt lesen"}
|
||||
</ActionButton>
|
||||
{dongleFehler && <p className="dm-fehler">{dongleFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
{dongle.stand?.gelesen_am
|
||||
// Der Punkt der Abkürzung ist zugleich der Satzpunkt - sonst
|
||||
// stünde dort "vor 1 Std..".
|
||||
? `Gelesen ${standAlter(dongle.stand.gelesen_am).replace(/\.$/, "")}. `
|
||||
: "Noch nicht gelesen. "}
|
||||
Der Regler oben schreibt den Schlaf-Timeout sofort hierher; bis das Gerät sich
|
||||
das nächste Mal meldet, steht die Änderung als „unterwegs" daneben.
|
||||
</span>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Darstellung</span>
|
||||
@@ -841,6 +745,10 @@ export function Einstellungen({
|
||||
)}
|
||||
|
||||
|
||||
{/* Installierte Fassung und ihr Update stehen in EINER Kachel: es ist
|
||||
dieselbe Sache aus zwei Blickwinkeln, und zwei Kacheln nebeneinander
|
||||
zwangen zum Vergleichen zweier Zahlen ueber eine Kachelgrenze hinweg
|
||||
(Vorgabe des Eigentuemers, 04.09.2026). */}
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Version</span>
|
||||
<Werteliste
|
||||
@@ -852,10 +760,6 @@ export function Einstellungen({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Integration-Update</span>
|
||||
{/* HIG "Loading": Netzwerk zu Gitea plus Entpacken dauert unbestimmt
|
||||
lang, ohne verlässliche Fortschrittsangabe - ein laufender
|
||||
Indikator statt eines nur deaktivierten Knopfes verhindert, dass
|
||||
@@ -966,17 +870,192 @@ export function Einstellungen({
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{/* Debug-Mode: Zugaenge, Dongle und der letzte Datensatz des Geraets.
|
||||
Drei Kacheln, die im Alltag nichts zu suchen haben und beim Suchen
|
||||
eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten
|
||||
(Vorgabe des Eigentuemers, 04.09.2026). */}
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Debug-Mode</span>
|
||||
<p className="dm-erklaerung">
|
||||
Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den das Gerät
|
||||
gemeldet hat. Für die Fehlersuche, nicht für den Alltag.
|
||||
</p>
|
||||
<ActionButton
|
||||
variant={debugOffen ? "primary" : "default"}
|
||||
onClick={() => setzeDebugOffen(!debugOffen)}
|
||||
>
|
||||
{debugOffen ? "Fertig" : "Öffnen"}
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{debugOffen && (
|
||||
<>
|
||||
{zugaenge.length > 0 && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Zugänge</span>
|
||||
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben —
|
||||
man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */}
|
||||
{zugaenge.map((z) => (
|
||||
<Accordion
|
||||
key={z.dienst}
|
||||
title={z.titel}
|
||||
summary={
|
||||
<span className={z.gesetzt ? "dm-token-gesetzt" : "dm-token-offen"}>
|
||||
{z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p className="dm-erklaerung">{z.zweck}</p>
|
||||
<Feld label="Neuer Token" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}
|
||||
value={tokenEntwurf[z.dienst] ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
disabled={tokenLaeuft !== null || !(tokenEntwurf[z.dienst] ?? "").trim()}
|
||||
onClick={() => void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")}
|
||||
>
|
||||
{tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"}
|
||||
</ActionButton>
|
||||
{z.gesetzt && (
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen("Token löschen?")) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
{tokenFehler && <p className="dm-fehler">{tokenFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist.
|
||||
Dieselben Felder gibt es unter Einstellungen → Geräte &
|
||||
Dienste → Audi Dashboard → Konfigurieren.
|
||||
</span>
|
||||
</Tile>
|
||||
)}
|
||||
{dongle?.aktiv && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Dongle</span>
|
||||
{dongle.stand?.fehler && <p className="dm-fehler">{dongle.stand.fehler}</p>}
|
||||
{(dongle.stand?.werte ?? []).length > 0 && (
|
||||
<Werteliste
|
||||
kinder={(dongle.stand?.werte ?? []).map((w) => (
|
||||
<Wertzeile
|
||||
key={w.schluessel}
|
||||
label={`${w.titel} ${w.nummer}`}
|
||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||
warnung={Boolean(w.abweichung)}
|
||||
zusatz={
|
||||
w.offen != null
|
||||
? `→ ${w.offen}, unterwegs`
|
||||
: w.abweichung
|
||||
? `(wir: ${w.unser})`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
<ActionButton disabled={dongleLaeuft} onClick={() => void dongleAbrufen()}>
|
||||
{dongleLaeuft ? "Liest …" : "Jetzt lesen"}
|
||||
</ActionButton>
|
||||
{dongleFehler && <p className="dm-fehler">{dongleFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
{dongle.stand?.gelesen_am
|
||||
// Der Punkt der Abkürzung ist zugleich der Satzpunkt - sonst
|
||||
// stünde dort "vor 1 Std..".
|
||||
? `Gelesen ${standAlter(dongle.stand.gelesen_am).replace(/\.$/, "")}. `
|
||||
: "Noch nicht gelesen. "}
|
||||
Der Regler oben schreibt den Schlaf-Timeout sofort hierher; bis das Gerät sich
|
||||
das nächste Mal meldet, steht die Änderung als „unterwegs" daneben.
|
||||
</span>
|
||||
</Tile>
|
||||
)}
|
||||
<LetzterDatensatzKachel datensatz={letzterDatensatz} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
|
||||
{entwurf && (
|
||||
<div className="dm-speicherleiste">
|
||||
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
|
||||
{laeuft ? "Speichere …" : "Änderungen speichern"}
|
||||
</ActionButton>
|
||||
<button type="button" className="dm-textknopf" onClick={() => setzeEntwurf(null)}>
|
||||
<ActionButton onClick={() => setzeEntwurf(null)} disabled={laeuft}>
|
||||
Verwerfen
|
||||
</button>
|
||||
</ActionButton>
|
||||
<ActionButton variant="primary" onClick={() => void speichern()} disabled={laeuft}>
|
||||
{laeuft ? "Speichere …" : "Speichern"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* Der letzte Datensatz des Dongles. Bewusst eine eigene Komponente: die Liste
|
||||
ist lang, hat mit den uebrigen Einstellungen nichts zu tun, und so bleibt
|
||||
die Hauptkomponente lesbar. */
|
||||
function LetzterDatensatzKachel({ datensatz }: { datensatz: LetzterDatensatz | null }) {
|
||||
if (!datensatz) {
|
||||
return (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzter Datensatz</span>
|
||||
<p className="dm-erklaerung">
|
||||
Noch nichts gemeldet. Dafür braucht es die Meldezeit oder das Fahrtsignal im Setup —
|
||||
und mindestens einen Datensatz des Geräts, seit Home Assistant läuft.
|
||||
</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
const rueckstand =
|
||||
datensatz.meldezeit && datensatz.angekommen
|
||||
? Math.round(
|
||||
(Date.parse(datensatz.angekommen) - Date.parse(datensatz.meldezeit)) / 1000,
|
||||
)
|
||||
: null
|
||||
return (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzter Datensatz</span>
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Gerät" wert={datensatz.geraet} />
|
||||
<Wertzeile label="Angekommen" wert={datumZeit(datensatz.angekommen)} />
|
||||
<Wertzeile
|
||||
label="Gerätezeit"
|
||||
wert={datensatz.meldezeit ? datumZeit(datensatz.meldezeit) : "–"}
|
||||
zusatz={rueckstand !== null ? `Rückstand ${de(rueckstand)} s` : undefined}
|
||||
/>
|
||||
{datensatz.werte.map((w) => (
|
||||
<Wertzeile
|
||||
key={w.name}
|
||||
label={w.name}
|
||||
wert={w.einheit ? `${w.wert} ${w.einheit}` : w.wert}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<p className="dm-erklaerung">
|
||||
{de(datensatz.gesamt)} Werte aus derselben Meldung. Was das Gerät in diesem Datensatz
|
||||
nicht geschickt hat, steht hier nicht — es behält in Home Assistant seinen alten Wert.
|
||||
Nach einem Neustart ist „Angekommen“ der Zeitpunkt des Neustarts, nicht der einer
|
||||
Übertragung — der Rückstand sagt dann, wie lange das Gerät schon schweigt.
|
||||
</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,3 +229,46 @@ export async function bildVerkleinern(datei: File): Promise<Blob> {
|
||||
}
|
||||
return datei
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Fahrzeugfotos einmal im Hintergrund holen und dekodieren.
|
||||
*
|
||||
* WARUM
|
||||
* -----
|
||||
* Gemeldet als "Bilder laden beim Umschalten der Menues einige ms verzoegert".
|
||||
* Innerhalb einer Sitzung liegt das nicht am Netz - die Adresse ist stabil und
|
||||
* Home Assistant liefert `/local/` mit `max-age` von 31 Tagen, der Abruf kommt
|
||||
* also aus dem Zwischenspeicher. Verzoegert wird das BILD selbst: ein frisch
|
||||
* erzeugtes <img> wird standardmaessig asynchron dekodiert und erst danach
|
||||
* gezeichnet, und beim Seitenwechsel entsteht jedes Mal ein neues.
|
||||
*
|
||||
* `decode()` erledigt genau diesen Schritt vorab. Danach zeichnet dasselbe
|
||||
* Bild beim naechsten Einhaengen sofort.
|
||||
*
|
||||
* WANN
|
||||
* ----
|
||||
* In der Leerlaufzeit nach dem ersten Datenladen, nicht beim Start - sonst
|
||||
* konkurriert es mit dem Abruf von Profil, Fahrten und Tankvorgaengen, den der
|
||||
* Nutzer wirklich braucht. Das Datenvolumen ist dasselbe wie beim Durchblaettern
|
||||
* aller Seiten, nur frueher und ohne Warten.
|
||||
*/
|
||||
export function bilderVorladen(): void {
|
||||
if (typeof window === "undefined" || typeof Image === "undefined") return
|
||||
const starten = () => {
|
||||
const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei]
|
||||
for (const name of namen) {
|
||||
const url = bildUrl(name)
|
||||
if (!url) continue
|
||||
const bild = new Image()
|
||||
bild.src = url
|
||||
// Ein fehlendes Foto ist der Normalfall (nicht jeder Platz ist belegt) -
|
||||
// deshalb still schlucken statt zu melden.
|
||||
void bild.decode?.().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
const leerlauf = (window as unknown as {
|
||||
requestIdleCallback?: (r: () => void, o?: { timeout: number }) => number
|
||||
}).requestIdleCallback
|
||||
if (leerlauf) leerlauf(starten, { timeout: 4000 })
|
||||
else window.setTimeout(starten, 2000)
|
||||
}
|
||||
|
||||
@@ -1590,8 +1590,11 @@ button.dm-listenzeile {
|
||||
background: none;
|
||||
border-top: 0;
|
||||
}
|
||||
.dm-speicherleiste--kachel > :first-child {
|
||||
flex: 1 1 auto;
|
||||
/* Beide Knoepfe gleich breit: ein Paar aus Bestaetigen und Abbrechen ist
|
||||
eine Entscheidung zwischen zwei Wegen, keine Haupt- und Nebensache. Welcher
|
||||
davon der gewollte ist, sagt die Fuellung, nicht die Groesse. */
|
||||
.dm-speicherleiste > * {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.dm-szene__zusatz {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.8","sha256":"d6fe041f31480ff67d9b39884e9ef8b5ac5795d8575716d4638a9f557198bc49","bytes":319995,"gebaut":"2026-09-04T09:50:03Z"}
|
||||
{"version":"2026.9.4.11","sha256":"937302e85e87c62326c4f7013cb026f50243b8cc5d521d752b5a3708120d2b91","bytes":320679,"gebaut":"2026-09-04T10:13:31Z"}
|
||||
Binary file not shown.
@@ -503,6 +503,33 @@ function bildInfo(i) {
|
||||
}
|
||||
return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
|
||||
}
|
||||
/* Alle Fahrzeugfotos einmal vorab holen UND dekodieren.
|
||||
|
||||
Gemeldet als "Bilder laden beim Umschalten der Menues verzoegert". Am Netz
|
||||
liegt es nicht: die Adresse ist innerhalb einer Sitzung stabil und Home
|
||||
Assistant liefert /local/ mit 31 Tagen Cache-Dauer. Verzoegert wird das Bild
|
||||
selbst - ein frisch erzeugtes <img> wird standardmaessig asynchron dekodiert
|
||||
und erst danach gezeichnet, und render() erzeugt bei jedem Seitenwechsel ein
|
||||
neues. decode() erledigt genau diesen Schritt vorab.
|
||||
|
||||
Laeuft in der Leerlaufzeit, damit es nicht mit dem ersten Datenabruf
|
||||
konkurriert, und nur einmal je Bildstand. */
|
||||
let bilderVorgeladen = null;
|
||||
function bilderVorladen() {
|
||||
if (bilderVorgeladen === bildVersion) return;
|
||||
bilderVorgeladen = bildVersion;
|
||||
const starten = () => {
|
||||
for (const datei of [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]) {
|
||||
const img = new Image();
|
||||
img.src = `/local/bilder/${datei}?v=${bildVersion}`;
|
||||
// Ein nicht hochgeladenes Foto ist der Normalfall - still schlucken.
|
||||
if (img.decode) img.decode().catch(() => {});
|
||||
}
|
||||
};
|
||||
if (window.requestIdleCallback) window.requestIdleCallback(starten, { timeout: 4000 });
|
||||
else setTimeout(starten, 2000);
|
||||
}
|
||||
|
||||
function carImg(modus) {
|
||||
if (modus === "fest") {
|
||||
const info = bildInfo(startIndex());
|
||||
@@ -3242,28 +3269,6 @@ function vEinst() {
|
||||
` : ""}
|
||||
<button class="aktion ${einrichtenOffen ? "primaer" : ""}" style="margin-top:16px" data-einrichten-auf>${einrichtenOffen ? "Fertig" : "Einrichten"}</button>
|
||||
</div>
|
||||
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
|
||||
${ZUGAENGE.map((z, i) => `
|
||||
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
|
||||
<span class="k">${esc(z.titel)}</span>
|
||||
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
|
||||
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
|
||||
}</span></button>
|
||||
<div class="acc-b" id="zug${i}">
|
||||
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
|
||||
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
|
||||
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
|
||||
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
|
||||
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
|
||||
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
|
||||
</div>`).join("")}
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist.
|
||||
Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard →
|
||||
Konfigurieren.</span>
|
||||
</div>` : ""}
|
||||
${(() => {
|
||||
/* Der Regler schreibt NICHT mehr beim Loslassen. Sein Wert stellt zugleich
|
||||
den Schlaf-Timeout des Dongles - eine Änderung am Fahrzeug, die über zwei
|
||||
@@ -3280,8 +3285,10 @@ function vEinst() {
|
||||
aria-label="Minuten bis zum automatischen Fahrtende">
|
||||
<output>${minuten} Min.</output>
|
||||
</div>
|
||||
${geaendert ? `<button class="aktion primaer" style="margin-top:14px" data-pausenzeitspeichern>Speichern</button>
|
||||
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>` : ""}
|
||||
${geaendert ? `<div class="knopfpaar">
|
||||
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>
|
||||
<button class="aktion primaer" data-pausenzeitspeichern>Speichern</button>
|
||||
</div>` : ""}
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
|
||||
@@ -3291,36 +3298,6 @@ function vEinst() {
|
||||
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
|
||||
</div>`;
|
||||
})()}
|
||||
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
|
||||
${DONGLE.stand && DONGLE.stand.fehler
|
||||
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
|
||||
: ""}
|
||||
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
|
||||
${DONGLE.stand.werte.map((w) => `<div class="row">
|
||||
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
|
||||
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
|
||||
w.geraet === null || w.geraet === undefined ? "–" : esc(String(w.geraet))
|
||||
}${w.einheit ? " " + esc(w.einheit) : ""}${
|
||||
w.offen !== null && w.offen !== undefined
|
||||
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
|
||||
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
|
||||
}</dd>
|
||||
</div>`).join("")}
|
||||
</dl>` : ""}
|
||||
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
|
||||
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
|
||||
}</button>
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
${DONGLE.stand && DONGLE.stand.gelesen_am
|
||||
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
|
||||
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
|
||||
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
|
||||
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
|
||||
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
|
||||
„unterwegs" daneben.</span>
|
||||
</div>` : ""}
|
||||
<div class="tile"><span class="label">Darstellung</span>
|
||||
<div class="seg">
|
||||
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
|
||||
@@ -3404,12 +3381,14 @@ function vEinst() {
|
||||
<label class="aktion" style="display:block;text-align:center;cursor:pointer">
|
||||
Backup importieren<input type="file" accept="application/json" data-backup="import" hidden></label>
|
||||
</div>
|
||||
<!-- Installierte Fassung und ihr Update stehen in EINER Kachel: es ist
|
||||
dieselbe Sache aus zwei Blickwinkeln, und zwei Kacheln nebeneinander
|
||||
zwangen zum Vergleichen zweier Zahlen ueber eine Kachelgrenze hinweg
|
||||
(Vorgabe des Eigentuemers, 04.09.2026). -->
|
||||
<div class="tile"><span class="label">Version</span>
|
||||
<dl class="rows">
|
||||
<div class="row" style="border-bottom:0"><dt>Installiert</dt><dd>${esc(APP_VERSION || "unbekannt")}<small>Home-Assistant-Integration</small></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="tile"><span class="label">Integration-Update</span>
|
||||
${(() => {
|
||||
const u = INTEGRATION_UPDATE;
|
||||
const laeuft = INTEGRATION_UPDATE_LAEUFT;
|
||||
@@ -3460,11 +3439,112 @@ function vEinst() {
|
||||
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler && !INTEGRATION_UPDATE_LAEUFT
|
||||
? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(INTEGRATION_UPDATE.fehler)}</span>`
|
||||
: ""}
|
||||
</div>`;
|
||||
</div>
|
||||
<!-- Debug-Mode: Zugaenge, Dongle und der letzte Datensatz des Geraets.
|
||||
Drei Kacheln, die im Alltag nichts zu suchen haben und beim Suchen
|
||||
eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten
|
||||
(Vorgabe des Eigentuemers, 04.09.2026). -->
|
||||
<div class="tile"><span class="label">Debug-Mode</span>
|
||||
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den
|
||||
das Gerät gemeldet hat. Für die Fehlersuche, nicht für den Alltag.</span>
|
||||
<button class="aktion ${debugOffen ? "primaer" : ""}" style="margin-top:16px" data-debug-auf>${debugOffen ? "Fertig" : "Öffnen"}</button>
|
||||
</div>
|
||||
${debugOffen ? `
|
||||
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
|
||||
${ZUGAENGE.map((z, i) => `
|
||||
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
|
||||
<span class="k">${esc(z.titel)}</span>
|
||||
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
|
||||
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
|
||||
}</span></button>
|
||||
<div class="acc-b" id="zug${i}">
|
||||
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
|
||||
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
|
||||
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
|
||||
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
|
||||
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
|
||||
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
|
||||
</div>`).join("")}
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist.
|
||||
Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard →
|
||||
Konfigurieren.</span>
|
||||
</div>` : ""}
|
||||
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
|
||||
${DONGLE.stand && DONGLE.stand.fehler
|
||||
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
|
||||
: ""}
|
||||
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
|
||||
${DONGLE.stand.werte.map((w) => `<div class="row">
|
||||
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
|
||||
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
|
||||
w.geraet === null || w.geraet === undefined ? "–" : esc(String(w.geraet))
|
||||
}${w.einheit ? " " + esc(w.einheit) : ""}${
|
||||
w.offen !== null && w.offen !== undefined
|
||||
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
|
||||
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
|
||||
}</dd>
|
||||
</div>`).join("")}
|
||||
</dl>` : ""}
|
||||
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
|
||||
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
|
||||
}</button>
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
${DONGLE.stand && DONGLE.stand.gelesen_am
|
||||
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
|
||||
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
|
||||
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
|
||||
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
|
||||
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
|
||||
„unterwegs" daneben.</span>
|
||||
</div>` : ""}
|
||||
${(() => {
|
||||
/* Der letzte Datensatz, wie er in Home Assistant angekommen ist - siehe
|
||||
geraet.py. Der Abstand zwischen Gerätezeit und Ankunft ist die Angabe,
|
||||
die bei gepufferten Datensätzen zählt: genau dieser Rückstand war die
|
||||
Wurzel des 3-km-Fahrt-Fehlers. */
|
||||
const d = LETZTER_DATENSATZ;
|
||||
if (!d) return `<div class="tile"><span class="label">Letzter Datensatz</span>
|
||||
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Noch nichts gemeldet. Dafür braucht es die Meldezeit oder das
|
||||
Fahrtsignal im Setup - und mindestens einen Datensatz des Geräts,
|
||||
seit Home Assistant läuft.</span></div>`;
|
||||
const rueckstand = d.meldezeit && d.angekommen
|
||||
? Math.round((Date.parse(d.angekommen) - Date.parse(d.meldezeit)) / 1000)
|
||||
: null;
|
||||
return `<div class="tile"><span class="label">Letzter Datensatz</span>
|
||||
<dl class="rows">
|
||||
<div class="row"><dt>Gerät</dt><dd>${esc(d.geraet || "unbekannt")}</dd></div>
|
||||
<div class="row"><dt>Angekommen</dt><dd>${dezeitpunkt(new Date(d.angekommen))}</dd></div>
|
||||
<div class="row"><dt>Gerätezeit</dt><dd>${d.meldezeit ? dezeitpunkt(new Date(d.meldezeit)) : "–"}${
|
||||
rueckstand !== null ? `<small>Rückstand ${de(rueckstand)} s</small>` : ""
|
||||
}</dd></div>
|
||||
${d.werte.map((w) => `<div class="row"><dt>${esc(w.name)}</dt><dd>${esc(String(w.wert))}${
|
||||
w.einheit ? " " + esc(w.einheit) : ""
|
||||
}</dd></div>`).join("")}
|
||||
</dl>
|
||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
${de(d.gesamt)} Werte aus derselben Meldung. Was das Gerät in diesem
|
||||
Datensatz nicht geschickt hat, steht hier nicht - es behält in Home
|
||||
Assistant seinen alten Wert. Nach einem Neustart ist „Angekommen" der
|
||||
Zeitpunkt des Neustarts, nicht der einer Übertragung — der Rückstand
|
||||
sagt dann, wie lange das Gerät schon schweigt.</span>
|
||||
</div>`;
|
||||
})()}
|
||||
` : ""}`;
|
||||
}
|
||||
|
||||
let nmOffen = false;
|
||||
let einrichtenOffen = false;
|
||||
let debugOffen = false;
|
||||
/* Entwurf des Reglers "Fahrt beenden" (Minuten) - null heisst: nicht angefasst.
|
||||
Er liegt bewusst NEBEN CONFIG.pausenzeit und nicht darin: erst der Knopf
|
||||
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
|
||||
@@ -4377,6 +4457,9 @@ let ZUGAENGE = [];
|
||||
/* Was am Dongle steht, was noch aussteht - siehe flespi.py im Backend.
|
||||
null, solange kein flespi-Token hinterlegt ist. */
|
||||
let DONGLE = null;
|
||||
/* Der letzte Datensatz, den das Geraet gemeldet hat - siehe geraet.py im
|
||||
Backend. null, solange weder Meldezeit noch Fahrtsignal zugeordnet sind. */
|
||||
let LETZTER_DATENSATZ = null;
|
||||
// true, waehrend "Jetzt lesen" laeuft. Wie INTEGRATION_UPDATE_LAEUFT und aus
|
||||
// demselben Grund: der Abruf geht ueber zwei Netze (wir -> flespi -> Geraet)
|
||||
// und darf nicht wie ein toter Knopf aussehen.
|
||||
@@ -5977,6 +6060,7 @@ function ereignisseVerdrahten() {
|
||||
}
|
||||
if (e.target.closest("[data-nmauf]")) { nmOffen = !nmOffen; render(); return; }
|
||||
if (e.target.closest("[data-einrichten-auf]")) { einrichtenOffen = !einrichtenOffen; render(); return; }
|
||||
if (e.target.closest("[data-debug-auf]")) { debugOffen = !debugOffen; render(); return; }
|
||||
if (e.target.closest("[data-nmspeichern]")) {
|
||||
const wert = parseInt($("#nm").value, 10);
|
||||
if (!isNaN(wert) && wert > 0) { CAR.reifen.anzugsmoment = wert; profilSpeichern(); }
|
||||
@@ -6701,6 +6785,9 @@ async function datenLaden(erzwingen) {
|
||||
DONGLE = versionState.attributes.daten
|
||||
? versionState.attributes.daten.dongle ?? null
|
||||
: null;
|
||||
LETZTER_DATENSATZ = versionState.attributes.daten
|
||||
? versionState.attributes.daten.letzter_datensatz ?? null
|
||||
: null;
|
||||
}
|
||||
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
|
||||
|
||||
@@ -6743,6 +6830,9 @@ async function datenLaden(erzwingen) {
|
||||
nachladeVersuche = 0;
|
||||
|
||||
render();
|
||||
// Erst nach dem ersten Zeichnen: die Fotos sollen den Aufbau der Seite
|
||||
// nicht ausbremsen, sondern den naechsten Seitenwechsel beschleunigen.
|
||||
bilderVorladen();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1657,3 +1657,17 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ein Paar aus Bestaetigen und Abbrechen steht nebeneinander, beide gleich
|
||||
breit: es ist eine Entscheidung zwischen zwei Wegen, keine Haupt- und
|
||||
Nebensache. Welcher der gewollte ist, sagt die Fuellung, nicht die Groesse -
|
||||
und der bestaetigende steht rechts, wie ueberall in iOS. */
|
||||
.knopfpaar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: var(--sp-4);
|
||||
}
|
||||
.knopfpaar .aktion {
|
||||
flex: 1 1 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Der letzte Datensatz des Dongles, so wie er in Home Assistant angekommen ist.
|
||||
|
||||
WOFUER
|
||||
------
|
||||
Fuer die Fehlersuche am Fahrzeug, nicht fuer den taeglichen Gebrauch: Steht das
|
||||
Fahrzeug wirklich, oder ist nur die Meldung alt? Liefert das Geraet gerade
|
||||
ueberhaupt CAN-Werte? Die Antwort steht in Home Assistants Zustaenden, aber
|
||||
verteilt auf siebzig Entitaeten - hier zusammengefasst an einer Stelle, die
|
||||
beide Oberflaechen ohne eigene Registerabfrage lesen koennen.
|
||||
|
||||
WAS "EIN DATENSATZ" IST
|
||||
-----------------------
|
||||
Die flespi-Integration setzt die Entitaeten EINES Geraetedatensatzes
|
||||
nacheinander, aber innerhalb weniger Millisekunden (am 01.09.2026 gemessen:
|
||||
Zuendung 07:35:23.633, zugehoerige Meldezeit 07:35:23.635 - siehe Abschnitt AY
|
||||
der AGENTS.md). Der letzte Datensatz ist deshalb: alle Entitaeten des Geraets,
|
||||
deren letzte Aktualisierung hoechstens FENSTER_S nach der juengsten liegt.
|
||||
|
||||
Das ist bewusst eine Naeherung ueber die ANKUNFTSzeit. Die Zeit des Geraets
|
||||
steht daneben (aus MELDEZEIT_SENSOR), und genau der Abstand zwischen beiden ist
|
||||
die Angabe, die bei gepufferten Datensaetzen interessiert - er war die Wurzel
|
||||
des 3-km-Fahrt-Fehlers (Abschnitt CB).
|
||||
|
||||
WELCHES GERAET
|
||||
--------------
|
||||
Nicht geraten: genommen wird das Geraet der Entitaet, die MELDEZEIT_SENSOR
|
||||
zugeordnet ist, sonst das des Fahrtsignals. Ist keines von beiden zugeordnet,
|
||||
gibt es nichts zu zeigen - dann bleibt der Rueckgabewert None, statt
|
||||
irgendein Geraet der Instanz zu waehlen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import verlauf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .einstellungen import Sensorzuordnung
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Wie weit ein Wert hinter dem juengsten liegen darf, um noch zum selben
|
||||
# Datensatz gezaehlt zu werden. Gemessen wurden Millisekunden; zwei Sekunden
|
||||
# sind grosszuegig genug fuer eine langsame Instanz und immer noch weit von der
|
||||
# kuerzesten Sendeperiode des Geraets entfernt (10 s beim Fahren).
|
||||
FENSTER_S = 2.0
|
||||
|
||||
# Ein Datensatz des FMM003 traegt je nach Konfiguration einige Dutzend Werte.
|
||||
# Die Obergrenze ist kein fachliches Limit, sondern verhindert, dass eine
|
||||
# fehlerhafte Zuordnung die veroeffentlichten Attribute aufblaeht.
|
||||
MAX_WERTE = 80
|
||||
|
||||
|
||||
def _meldezeit(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
|
||||
"""Die Zeit, die das Geraet dem letzten Datensatz gegeben hat."""
|
||||
if not werte.MELDEZEIT_SENSOR:
|
||||
return None
|
||||
zustand = hass.states.get(werte.MELDEZEIT_SENSOR)
|
||||
if zustand is None:
|
||||
return None
|
||||
try:
|
||||
gemeldet = datetime.fromtimestamp(float(zustand.state), tz=timezone.utc)
|
||||
except (TypeError, ValueError, OSError, OverflowError):
|
||||
return None
|
||||
# Dieselbe Pruefung wie in der Fahrterkennung: eine Sensor-ID statt eines
|
||||
# Zeitstempels ergibt sonst einen Zeitpunkt kurz nach 1970 (Abschnitt BO).
|
||||
if not verlauf.geraetezeit_plausibel(gemeldet, datetime.now(timezone.utc)):
|
||||
return None
|
||||
return gemeldet.isoformat()
|
||||
|
||||
|
||||
def _geraet_id(hass: HomeAssistant, werte: Sensorzuordnung) -> str | None:
|
||||
register = er.async_get(hass)
|
||||
for eid in (werte.MELDEZEIT_SENSOR, verlauf.fahrtsignal(werte), werte.ZUENDUNG_SENSOR):
|
||||
if not eid:
|
||||
continue
|
||||
eintrag = register.async_get(eid)
|
||||
if eintrag and eintrag.device_id:
|
||||
return eintrag.device_id
|
||||
return None
|
||||
|
||||
|
||||
def letzter_datensatz(hass: HomeAssistant, werte: Sensorzuordnung) -> dict[str, Any] | None:
|
||||
"""Die Werte der juengsten Meldung des Dongles - None, wenn keine da ist."""
|
||||
geraet_id = _geraet_id(hass, werte)
|
||||
if not geraet_id:
|
||||
return None
|
||||
|
||||
register = er.async_get(hass)
|
||||
eintraege = er.async_entries_for_device(register, geraet_id, include_disabled_entities=False)
|
||||
zustaende = []
|
||||
for eintrag in eintraege:
|
||||
zustand = hass.states.get(eintrag.entity_id)
|
||||
# unavailable/unknown tragen keinen Wert - sie stehen fuer eine
|
||||
# Entitaet, die das Geraet in diesem Datensatz nicht gemeldet hat.
|
||||
if zustand is None or zustand.state in ("unavailable", "unknown"):
|
||||
continue
|
||||
zustaende.append(zustand)
|
||||
if not zustaende:
|
||||
return None
|
||||
|
||||
juengste = max(z.last_updated for z in zustaende)
|
||||
grenze = juengste - timedelta(seconds=FENSTER_S)
|
||||
im_datensatz = sorted(
|
||||
(z for z in zustaende if z.last_updated >= grenze),
|
||||
key=lambda z: z.attributes.get("friendly_name") or z.entity_id,
|
||||
)
|
||||
|
||||
geraete_register = dr.async_get(hass)
|
||||
geraet = geraete_register.async_get(geraet_id)
|
||||
name = (geraet.name if geraet else None) or geraet_id
|
||||
|
||||
def kurz(voll: str) -> str:
|
||||
"""Der Geraetename steht schon in der Kopfzeile - hier nur das Signal."""
|
||||
rest = voll[len(name):].strip() if voll.startswith(name) else voll
|
||||
return rest or voll
|
||||
|
||||
return {
|
||||
"geraet": name,
|
||||
"angekommen": juengste.isoformat(),
|
||||
# Die Zeit, die das GERAET dem Datensatz gegeben hat. Der Abstand zur
|
||||
# Ankunft ist der Puffer-Rueckstand.
|
||||
"meldezeit": _meldezeit(hass, werte),
|
||||
"werte": [
|
||||
{
|
||||
"name": kurz(z.attributes.get("friendly_name") or z.entity_id),
|
||||
"wert": z.state,
|
||||
"einheit": z.attributes.get("unit_of_measurement"),
|
||||
}
|
||||
for z in im_datensatz[:MAX_WERTE]
|
||||
],
|
||||
"gesamt": len(im_datensatz),
|
||||
}
|
||||
@@ -51,6 +51,7 @@ from . import (
|
||||
batterie,
|
||||
fahrterkennung,
|
||||
flespi,
|
||||
geraet,
|
||||
identitaet,
|
||||
reifen,
|
||||
screening,
|
||||
@@ -253,6 +254,12 @@ class Koordinator:
|
||||
await self.profil_veroeffentlichen()
|
||||
await self.fahrten_veroeffentlichen()
|
||||
await self.tankvorgaenge_veroeffentlichen()
|
||||
# Muss mitlaufen, seit dort der letzte Datensatz des Dongles steht:
|
||||
# beim Start haben dessen Entitaeten noch gar keinen Zustand, die
|
||||
# Angabe waere sonst dauerhaft leer. Kostet einen kleinen Dateilesevorgang
|
||||
# (bundle.json) je Minute und KEINEN Netzaufruf - die Pruefung bei Gitea
|
||||
# bleibt an ihrem Dienst, siehe Abschnitt J.
|
||||
await self.app_version_veroeffentlichen()
|
||||
|
||||
async def _takt_batterie(self, _jetzt: datetime.datetime) -> None:
|
||||
await batterie.pruefen(self)
|
||||
@@ -636,6 +643,12 @@ class Koordinator:
|
||||
"aktiv": flespi.zugang(self) is not None,
|
||||
"stand": self.flespi_stand,
|
||||
},
|
||||
# Der letzte Datensatz des Dongles, wie er hier angekommen
|
||||
# ist - fuer die Fehlersuche, siehe geraet.py. None, solange
|
||||
# weder Meldezeit noch Fahrtsignal zugeordnet sind.
|
||||
"letzter_datensatz": geraet.letzter_datensatz(
|
||||
self.hass, self.zuordnung.werte
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.8",
|
||||
"version": "2026.9.4.11",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
Reference in New Issue
Block a user