Zugaenge: die Dienste-Token aus App und Panel bedienbar (2026.9.3.8)

Die Token liegen seit jeher in entry.options, geschrieben vom Options-Flow
unter Einstellungen -> Geraete & Dienste -> Konfigurieren. Der Weg bleibt und
ist der Rueckfall, wenn die App den Server nicht erreicht - genau der Fall, in
dem ein abgelaufener Token auffaellt. Erreichbar ist er aber nur ueber HAs
eigene Weboberflaeche; die iOS-App zeigt keine Konfigurationsdialoge. Die neue
Kachel ist die Bedienflaeche dafuer, kein zweiter Speicherort.

Der Wert kommt nie zurueck: veroeffentlicht werden nur "gesetzt/nicht gesetzt"
und die letzten vier Zeichen (unter zwoelf Zeichen Laenge gar nichts). Auch das
Protokoll bekommt ihn nicht, und das Panel leert das Eingabefeld sofort nach
dem Absenden.

Bewusst NICHT im Fahrzeugprofil: sicherung.py sichert fahrzeugprofil.json, und
"Backup exportieren" laedt es als Datei auf das Geraet - ein Token dort wanderte
in jede Sicherung und jede weitergegebene Datei.

Ein Dienst statt zwei: zugang_setzen(dienst, token), leerer Text loescht. In der
App nicht ueber die Warteschlange - ein Stunden spaeter nachgereichter Token
ueberschriebe womoeglich einen inzwischen eingetragenen neuen.

CONF_FLESPI_TOKEN ist neu und noch ohne Leser; der Flespi-Weg zum Auslesen der
Geraetekonfiguration ist damit vorbereitet, aber nicht gebaut.

Verifiziert: py_compile auf sechs Backend-Dateien, Panel als Modul geparst,
companion-app tsc sauber und 179/179, audi_ha_test auf 2026.9.3.8 sauber
gestartet. Live am laufenden Panel der ganze Kreis, Container hinterher wie
vorgefunden: Wegwerf-Token gesetzt -> "gesetzt . ...0000", der Wert selbst
taucht in den veroeffentlichten Attributen nirgends auf (darauf geprueft),
ueber die Sicherheitsabfrage geloescht -> wieder "nicht gesetzt", der
Gitea-Token unberuehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:08:55 +02:00
parent 5ccbfec92d
commit 8c96d9b78e
16 changed files with 400 additions and 8 deletions
+61 -1
View File
@@ -1,6 +1,7 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-03** („Fahrt beenden auf Knopfdruck mit vorlaeufigem Ende, Schieberegler in
**Last updated: 2026-09-03** (Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
zurueck, `2026.9.3.8`, Abschnitt BY. Davor: „Fahrt beenden“ auf Knopfdruck mit vorlaeufigem Ende, Schieberegler in
5-Minuten-Schritten und im Tagmodus sichtbar, `2026.9.3.7`, Abschnitt BX.
Davor: Die Fahrt endet wieder bei uns statt im Dongle - Zuendung als Ausloeser,
eigene Wartezeit, Nachlauf des Geraets abgezogen, Schieberegler, `2026.9.3.5`, Abschnitt BW.
@@ -8942,3 +8943,62 @@ treffen sich bequem, und minutengenau ist hier ohnehin Scheingenauigkeit.
Der Daumen war im **Tagmodus** kaum zu sehen: weiss auf weisser Kachel, nur ein Schatten. Er hat
jetzt `border: 1px solid var(--line-strong)` — ein Token, zwei Themen: im Tagmodus zeichnet es ihn
ab, im Nachtmodus ist es auf Weiss unsichtbar, wo es nicht gebraucht wird.
---
## BY. Zugänge: die Dienste-Token aus der App bedienbar (2026.9.3.8)
### Warum es das braucht, obwohl es den Options-Flow schon gibt
`CONF_GITEA_TOKEN` liegt seit Abschnitt J in `entry.options`, geschrieben vom Options-Flow unter
Einstellungen → Geräte & Dienste → Audi Dashboard → Konfigurieren. Der Weg bleibt — er ist der
**Rückfall**, wenn die App den Server gerade nicht erreicht, also genau der Fall, in dem ein
abgelaufener Token auffällt.
Er ist aber nur über Home Assistants eigene Weboberfläche erreichbar. Die iOS-App zeigt keine
HA-Konfigurationsdialoge; läuft unterwegs ein Token ab, müsste man einen Browser öffnen. Die neue
Kachel ist die Bedienfläche dafür — **kein zweiter Speicherort**, sondern derselbe `entry.options`.
### Der Wert kommt nie zurück
`zugaenge.stand()` gibt ausschliesslich `gesetzt`/`nicht gesetzt` und die **letzten vier Zeichen**
heraus; unter zwölf Zeichen Länge gar nichts, weil vier Zeichen eines kurzen Tokens schon ein
nennenswerter Teil davon wären. Ein Token, der einmal drin ist, verlässt die Integration nicht
wieder — weder über die veröffentlichte Entität noch über einen Dienst. Auch das Protokoll bekommt
ihn nicht: `_LOGGER.info` nennt Dienst und Vorgang, nicht den Wert.
Das Panel leert das Eingabefeld unmittelbar nach dem Absenden, damit der Wert nicht im DOM
stehenbleibt.
### Warum die Token nicht ins Fahrzeugprofil dürfen
Das war der Punkt, an dem die naheliegende Lösung falsch gewesen wäre. `sicherung.py` sichert fünf
Dateien, darunter `fahrzeugprofil.json`, und „Backup exportieren" lädt Profil, Fahrten und
Tankvorgänge als **eine Datei** auf das Gerät. Läge ein Token im Profil, wanderte er in jede
Sicherung und in jede exportierte Datei, die man vielleicht weitergibt. Er bleibt deshalb dort, wo
Home Assistant Geheimnisse ohnehin hält.
### Ein Dienst, nicht zwei
`zugang_setzen(dienst, token)`**leerer Text löscht**. Ein eigener Lösch-Dienst wäre ein zweiter
Weg zu derselben Wirkung. In der App läuft der Aufruf bewusst **nicht** über die Warteschlange: ein
Token, der Stunden später aus dem Funkloch nachgereicht wird, überschriebe womöglich einen
inzwischen eingetragenen neuen.
`DIENSTE` in `zugaenge.py` führt beide Token mit Zweck und einem Merkmal `erforderlich` — heute
beide `False`. Ein Token, den die App zum Starten bräuchte, wäre hier ohnehin fehl am Platz: er
müsste eingetragen sein, bevor jemand diese Oberfläche überhaupt öffnen kann. `CONF_FLESPI_TOKEN`
ist neu und noch ohne Leser; der Flespi-Weg (Abschnitt AZ: `GET /gw/devices/{id}/settings/all`) ist
damit vorbereitet, aber nicht gebaut.
### Verifiziert
`py_compile` auf sechs Backend-Dateien, Panel als Modul geparst, companion-app `tsc` sauber und
179/179, `audi_ha_test` auf `2026.9.3.8` sauber gestartet.
**Live am laufenden Panel, ganzer Kreis, Container hinterher wie vorgefunden:** die Kachel zeigt
Gitea als `gesetzt · …bad2` in Grün und flespi als `nicht gesetzt` in Grau; aufgeklappt ein
Passwortfeld mit passendem Platzhalter und die Knöpfe Speichern/Löschen. Einen Wegwerf-Token
gesetzt → `gesetzt · …0000`, und **der Wert selbst taucht in den veröffentlichten Attributen
nirgends auf** (auf genau diese Zeichenkette geprüft, nicht nur angesehen). Danach über die
Sicherheitsabfrage „Token löschen?" entfernt → wieder `nicht gesetzt`, der Gitea-Token unberührt.
+10
View File
@@ -341,6 +341,16 @@ export class DataMetricApi {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "fahrt_jetzt_beenden", {});
}
/** Traegt einen Dienste-Token ein oder loescht ihn (leerer Text = loeschen).
Nicht ueber die Warteschlange: ein Token, der Stunden spaeter aus dem
Funkloch nachgereicht wird, ueberschriebe womoeglich einen inzwischen
eingetragenen neuen. Und der Wert kommt nie zurueck - was gesetzt ist,
steht in daten.zugaenge, der Token selbst nirgends. */
zugangSetzen(dienst: string, token: string): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "zugang_setzen", { dienst, token });
}
historieImportieren(start: string, ende: string): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", {
start,
+16
View File
@@ -219,11 +219,27 @@ export interface IntegrationUpdateAngabe {
fehler: string | null;
}
/** Ein Dienste-Token dieser Installation (zugaenge.py).
Der Token selbst ist hier BEWUSST nicht enthalten und kommt auch nie ueber
die Leitung: gezeigt wird nur, ob einer gesetzt ist, und seine letzten vier
Zeichen zum Wiedererkennen. */
export interface ZugangAngabe {
dienst: string;
titel: string;
zweck: string;
/** Laeuft die App ohne diesen Token gar nicht? Beide heutigen tun es. */
erforderlich: boolean;
gesetzt: boolean;
rest: string | null;
}
/** Nutzlast von sensor.audi_dashboard_app_version. */
export interface AppVersionAngabe {
app: string | null;
buendel: Buendelangabe | null;
integration_update: IntegrationUpdateAngabe | null;
zugaenge?: ZugangAngabe[];
}
/* -------------------------------------------------- Entitäts-Verzeichnis */
+5
View File
@@ -26,6 +26,7 @@ import {
type Fahrt,
type Fahrzeugstatus,
type IntegrationUpdateAngabe,
type ZugangAngabe,
type Profil,
type Tankvorgang,
type Verbindungszustand,
@@ -69,6 +70,7 @@ export interface DatenWert {
/** Ergebnis der letzten Selbst-Update-Prüfung/-Installation der
Integration (aktualisierung.py), oder null vor der ersten Prüfung. */
integrationUpdate: IntegrationUpdateAngabe | null
zugaenge: ZugangAngabe[]
einstellungen: Einstellungen | null
fahrzeug: Fahrzeug | null
@@ -115,6 +117,7 @@ export function DatenAnbieter({
const [warteschlange, setzeWarteschlange] = useState<readonly WartenderAuftrag[]>([])
const [serverVersion, setzeServerVersion] = useState<string | null>(null)
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
null,
)
@@ -158,6 +161,7 @@ export function DatenAnbieter({
setzeServerVersion(versionsangabe?.app ?? null)
setzeOtaBuendel(versionsangabe?.buendel ?? null)
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
setzeZugaenge(versionsangabe?.zugaenge ?? [])
return true
} catch (fehler) {
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
@@ -289,6 +293,7 @@ export function DatenAnbieter({
serverVersion,
otaBuendel,
integrationUpdate,
zugaenge,
einstellungen,
fahrzeug,
fahrten,
+85 -1
View File
@@ -6,7 +6,7 @@
import { useEffect, useRef, useState } from "react"
import { ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
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"
@@ -42,6 +42,7 @@ export function Einstellungen({
otaBuendel,
integrationUpdate,
serverVersion,
zugaenge,
verbindung,
api,
profilSpeichern,
@@ -223,6 +224,29 @@ export function Einstellungen({
}
}
/* Zugaenge: je Dienst ein Entwurf im Eingabefeld. Der gespeicherte Token
kommt NIE zurueck (siehe zugaenge.py), das Feld startet deshalb immer leer
und zeigt als Platzhalter, ob schon einer hinterlegt ist. */
const [tokenEntwurf, setzeTokenEntwurf] = useState<Record<string, string>>({})
const [tokenLaeuft, setzeTokenLaeuft] = useState<string | null>(null)
const [tokenFehler, setzeTokenFehler] = useState<string | null>(null)
const tokenSchreiben = async (dienst: string, wert: string) => {
setzeTokenLaeuft(dienst)
setzeTokenFehler(null)
try {
await api.zugangSetzen(dienst, wert)
setzeTokenEntwurf((alt) => ({ ...alt, [dienst]: "" }))
await neuLaden()
} catch (fehler) {
setzeTokenFehler(
fehler instanceof Error ? fehler.message : "Der Token konnte nicht gespeichert werden.",
)
} finally {
setzeTokenLaeuft(null)
}
}
const abmelden = async () => {
if (!bestaetigen("Zugang von diesem Gerät entfernen? Die Daten auf dem Server bleiben.")) return
await zugangVerwerfen()
@@ -655,6 +679,66 @@ export function Einstellungen({
</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 für ${z.titel} 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 in Home Assistant unter Einstellungen Geräte &amp;
Dienste Audi Dashboard Konfigurieren.
</span>
</Tile>
)}
<Tile>
<span className="ads-eyebrow">Version</span>
<Werteliste
+3
View File
@@ -1992,3 +1992,6 @@ button.dm-listenzeile {
/* Gruen wie der Sicherheits-Haken: es laeuft gerade etwas. */
.dm-blatt__k small .dm-laeuft, .dm-laeuft { color: var(--ok); }
.dm-vorlaeufig { color: var(--fg3); }
.dm-token-gesetzt { color: var(--ok); }
.dm-token-offen { color: var(--fg3); }
@@ -28,7 +28,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu
from homeassistant.core import callback
from homeassistant.helpers import selector
from .const import CONF_GITEA_TOKEN, DOMAIN
from .const import CONF_FLESPI_TOKEN, CONF_GITEA_TOKEN, DOMAIN
class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -54,10 +54,17 @@ class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
class AudiDashboardOptionsFlow(OptionsFlow):
"""Einstellungen -> Geräte & Dienste -> Audi Dashboard -> Konfigurieren.
Ein Feld: der Gitea-Token fürs Selbst-Update. Leer lassen deaktiviert
die Update-Prüfung wieder (aktualisierung.py verlangt einen nicht-leeren
Zwei Felder, beide freiwillig: der Gitea-Token fürs Selbst-Update und der
flespi-Token fürs Auslesen der Geräte-Konfiguration. Leer lassen schaltet
die jeweilige Funktion ab (aktualisierung.py verlangt einen nicht-leeren
Token, sonst gibt es eine verständliche Fehlermeldung statt eines
unbehandelten Fehlers)."""
unbehandelten Fehlers).
Dieser Weg bleibt der Rückfall neben der Kachel „Zugänge" in App und Panel
(siehe zugaenge.py): er ist der einzige, der auch dann trägt, wenn die App
den Server gerade nicht erreicht - genau der Fall, in dem ein abgelaufener
Token auffällt. Beide schreiben in dieselben entry.options, es gibt keinen
zweiten Wahrheitsort."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -72,6 +79,9 @@ class AudiDashboardOptionsFlow(OptionsFlow):
vol.Optional(CONF_GITEA_TOKEN, default=""): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Optional(CONF_FLESPI_TOKEN, default=""): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
}),
self.config_entry.options,
),
@@ -78,6 +78,7 @@ D_FAHRT_MANUELL_ANLEGEN = "fahrt_manuell_anlegen"
D_FAHRT_AKTUALISIEREN = "fahrt_aktualisieren"
D_FAHRT_LOESCHEN = "fahrt_loeschen"
D_FAHRT_JETZT_BEENDEN = "fahrt_jetzt_beenden"
D_ZUGANG_SETZEN = "zugang_setzen"
D_SCREENING_JETZT = "screening_jetzt"
D_BELEG_HOCHLADEN = "beleg_hochladen"
D_TANKVORGANG_MANUELL = "tankvorgang_manuell"
@@ -103,3 +104,4 @@ D_UPDATE_INSTALLIEREN = "update_installieren"
# in config_flow.py definiert, damit aktualisierung.py/dienste.py ihn ohne
# Umweg über den Options-Flow importieren können.
CONF_GITEA_TOKEN = "gitea_token"
CONF_FLESPI_TOKEN = "flespi_token"
@@ -38,6 +38,7 @@ from . import (
reifen,
screening,
sicherung,
zugaenge,
)
from .const import (
BILDER_ORDNER,
@@ -52,6 +53,7 @@ from .const import (
D_ENTITAETEN_SCHREIBEN,
D_FAHRT_AKTUALISIEREN,
D_FAHRT_JETZT_BEENDEN,
D_ZUGANG_SETZEN,
D_FAHRT_LOESCHEN,
D_FAHRT_MANUELL_ANLEGEN,
D_HISTORIE_IMPORTIEREN,
@@ -134,6 +136,12 @@ SCHEMATA: dict[str, vol.Schema] = {
}),
D_FAHRT_LOESCHEN: vol.Schema({vol.Required("trip_id"): cv.string}),
D_FAHRT_JETZT_BEENDEN: vol.Schema({}),
D_ZUGANG_SETZEN: vol.Schema({
vol.Required("dienst"): cv.string,
# Leerer Text loescht - ein eigener Loesch-Dienst waere ein zweiter Weg
# zu derselben Wirkung.
vol.Optional("token", default=""): cv.string,
}),
D_BELEG_HOCHLADEN: vol.Schema({
vol.Required("pdf_base64"): cv.string,
vol.Optional("dateiname"): _TEXT,
@@ -351,6 +359,9 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
lambda a: fahrterkennung.aktualisieren(k, dict(a.data))
),
D_FAHRT_JETZT_BEENDEN: _als_dienst(lambda _a: fahrterkennung.jetzt_beenden(k)),
D_ZUGANG_SETZEN: _als_dienst(
lambda a: zugaenge.setzen(k, a.data["dienst"], a.data.get("token", ""))
),
D_FAHRT_LOESCHEN: _als_dienst(
lambda a: fahrterkennung.loeschen(k, a.data["trip_id"])
),
@@ -1 +1 @@
{"version":"2026.9.3.7","sha256":"82f5fdbf1ab4281a51f0c2d31f27b7609b4bc88835c19641d071f9db4970bb00","bytes":268073,"gebaut":"2026-09-03T12:57:42Z"}
{"version":"2026.9.3.8","sha256":"f1d22bf45afb23685ed3c0a660b4ef5908e0194f19a1461af6ae928d0163ecdc","bytes":268607,"gebaut":"2026-09-03T13:06:29Z"}
@@ -3076,6 +3076,28 @@ 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 &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
<div class="tile"><span class="label">Fahrt beenden</span>
<div class="schieber" style="margin-top:14px">
<input type="range" min="0" max="60" step="5" value="${CONFIG.pausenzeit}"
@@ -4133,6 +4155,9 @@ let APP_VERSION = null;
// geprüft wurde. Ersetzt install.ps1 als Update-Weg, das Windows Smart App
// Control zuverlässig blockiert; siehe vVersion()/Einstellungen-Ansicht.
let INTEGRATION_UPDATE = null;
/* Nur "gesetzt/nicht gesetzt" je Dienst - der Token selbst kommt nie ueber
die Leitung, siehe zugaenge.py im Backend. */
let ZUGAENGE = [];
// "pruefen" | "installieren" | "neustart" | null - während einer dieser drei
// Aktionen läuft, siehe vEinst()'s Integration-Update-Kachel. Eigens statt
// über serviceRufen() (das feuert-und-vergisst), weil diese drei Aktionen
@@ -5976,6 +6001,31 @@ function ereignisseVerdrahten() {
// div ohne data-go, aber die Reihenfolge haelt das auch dann, wenn die
// Kachel spaeter einmal anders gebaut wird.
if (e.target.closest("[data-fahrtbeenden]")) { serviceRufen("fahrt_jetzt_beenden", {}); return; }
const tokSpeichern = e.target.closest("[data-tokenspeichern]");
if (tokSpeichern) {
const dienst = tokSpeichern.dataset.tokenspeichern;
const feld = ROOT.querySelector(`[data-tokenfeld="${dienst}"]`);
const wert = feld ? feld.value.trim() : "";
if (!wert) { hinweis("Kein Token eingegeben", "Bitte erst einen Token eintragen."); return; }
// Das Feld sofort leeren: der Wert ist ab hier nicht mehr unser Geschäft,
// und ein stehengebliebener Token im DOM wäre genau das, was zugaenge.py
// für die Rückgabe ausschließt.
if (feld) feld.value = "";
serviceRufen("zugang_setzen", { dienst, token: wert });
return;
}
const tokLoeschen = e.target.closest("[data-tokenloeschen]");
if (tokLoeschen) {
const dienst = tokLoeschen.dataset.tokenloeschen;
const titel = (ZUGAENGE.find((z) => z.dienst === dienst) || {}).titel || dienst;
bestaetigen(
"Token löschen?",
`Der hinterlegte Token für ${titel} wird entfernt. Die zugehörige Funktion ist danach abgeschaltet, bis ein neuer eingetragen wird.`,
"Löschen",
() => serviceRufen("zugang_setzen", { dienst, token: "" }),
);
return;
}
const g = e.target.closest("[data-go]");
if (g) { const [n, i] = g.dataset.go.split(":"); go(n, i); }
});
@@ -6338,6 +6388,9 @@ async function datenLaden(erzwingen) {
INTEGRATION_UPDATE = versionState.attributes.daten
? versionState.attributes.daten.integration_update ?? null
: null;
ZUGAENGE = versionState.attributes.daten
? versionState.attributes.daten.zugaenge ?? []
: [];
}
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
@@ -57,6 +57,7 @@ from . import (
sicherung,
tankerkennung,
verlauf,
zugaenge,
)
from .ablage import Ablage
from .geokodierung import Geokodierung
@@ -590,6 +591,10 @@ class Koordinator:
"app": self.version,
"buendel": buendel,
"integration_update": self.letzte_update_pruefung,
# Nur "gesetzt/nicht gesetzt" und die letzten vier Zeichen -
# der Token selbst verlaesst die Integration nie, siehe
# zugaenge.py.
"zugaenge": zugaenge.stand(self),
},
)
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.3.7",
"version": "2026.9.3.8",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
@@ -333,3 +333,15 @@ csv_importieren:
fahrt_jetzt_beenden:
fields: {}
zugang_setzen:
fields:
dienst:
required: true
example: gitea
selector:
text:
token:
required: false
selector:
text:
@@ -0,0 +1,121 @@
"""Die Dienste-Token dieser Installation - lesen, setzen, löschen.
WARUM DAS EINE EIGENE DATEI IST
-------------------------------
Es gibt für die Token längst einen Ort: `entry.options`, geschrieben vom
Options-Flow unter Einstellungen → Geräte & Dienste → Audi Dashboard →
Konfigurieren. Dieser Weg bleibt und ist der Rückfall, wenn die App den Server
gerade nicht erreicht - genau der Fall, in dem ein abgelaufener Token auffällt.
Er ist aber **nur über Home Assistants eigene Weboberfläche erreichbar**. Die
iOS-App zeigt keine HA-Konfigurationsdialoge; läuft dort unterwegs ein Token
ab, müsste man einen Browser öffnen. Diese Datei ist die Bedienfläche dafür -
kein zweiter Speicherort, sondern derselbe `entry.options`, nur von aussen
erreichbar.
DER WERT KOMMT NIE ZURÜCK
-------------------------
`stand()` gibt ausschliesslich "gesetzt/nicht gesetzt" und die letzten vier
Zeichen heraus. Ein Token, das einmal drin ist, verlässt die Integration nicht
wieder - weder über die veröffentlichte Entität noch über einen Dienst. Wer
ihn ersetzen will, trägt einen neuen ein; wer ihn loswerden will, löscht ihn.
Der letzte Rest reicht zum Wiedererkennen ("ist das der neue oder noch der
alte?") und ist für sich genommen nutzlos.
WARUM DIE TOKEN NICHT INS FAHRZEUGPROFIL DÜRFEN
-----------------------------------------------
`sicherung.py` sichert fünf Dateien, darunter `fahrzeugprofil.json`, und
"Backup exportieren" lädt Profil, Fahrten und Tankvorgänge als eine Datei auf
das Gerät des Nutzers. Läge ein Token im Profil, wanderte er in **jede**
Sicherung und in jede exportierte Datei. Er bleibt deshalb dort, wo Home
Assistant Geheimnisse ohnehin hält.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from .const import CONF_FLESPI_TOKEN, CONF_GITEA_TOKEN
if TYPE_CHECKING:
from .koordinator import Koordinator
_LOGGER = logging.getLogger(__name__)
# Die Token, die diese Installation kennt. Die Reihenfolge ist die der Anzeige.
#
# "erforderlich" sagt, ob die App ohne ihn nicht läuft. Beide sind es NICHT:
# ohne Gitea-Token entfällt nur die Update-Prüfung, ohne Flespi-Token nur das
# Auslesen der Geräte-Konfiguration. Ein Token, den die App zum Starten
# bräuchte, wäre hier ohnehin fehl am Platz - er müsste eingetragen sein, bevor
# jemand diese Oberfläche überhaupt öffnen kann.
DIENSTE: dict[str, dict] = {
"gitea": {
"schluessel": CONF_GITEA_TOKEN,
"titel": "Gitea",
"zweck": "Update-Prüfung und Selbst-Update der Integration.",
"erforderlich": False,
},
"flespi": {
"schluessel": CONF_FLESPI_TOKEN,
"titel": "flespi",
"zweck": "Liest die Konfiguration des Dongles (noch nicht in Gebrauch).",
"erforderlich": False,
},
}
def _rest(token: str) -> str | None:
"""Die letzten vier Zeichen - zum Wiedererkennen, nicht zum Benutzen.
Bei einem sehr kurzen Token wäre das schon ein nennenswerter Teil davon;
dann lieber gar nichts zeigen.
"""
return token[-4:] if len(token) >= 12 else None
def stand(k: Koordinator) -> list[dict]:
"""Was die Oberflächen zu sehen bekommen - niemals der Token selbst."""
return [
{
"dienst": name,
"titel": eintrag["titel"],
"zweck": eintrag["zweck"],
"erforderlich": eintrag["erforderlich"],
"gesetzt": bool(k.entry.options.get(eintrag["schluessel"], "")),
"rest": _rest(k.entry.options.get(eintrag["schluessel"], "")),
}
for name, eintrag in DIENSTE.items()
]
async def setzen(k: Koordinator, dienst: str, token: str) -> None:
"""Trägt einen Token ein oder löscht ihn (leerer Text = löschen).
Geschrieben wird in denselben `entry.options`, die auch der Options-Flow
benutzt - es gibt bewusst keinen zweiten Wahrheitsort. `async_update_entry`
lädt den Eintrag NICHT neu; das ist hier richtig, weil beide Leser
(`aktualisierung.py` und der künftige Flespi-Weg) die Option bei jedem
Aufruf frisch aus `entry.options` holen.
"""
eintrag = DIENSTE.get(dienst)
if eintrag is None:
_LOGGER.warning("Unbekannter Dienst %s - nichts geändert", dienst)
return
token = token.strip()
optionen = dict(k.entry.options)
if token:
optionen[eintrag["schluessel"]] = token
else:
optionen.pop(eintrag["schluessel"], None)
k.hass.config_entries.async_update_entry(k.entry, options=optionen)
# Ausdrücklich ohne den Wert und ohne seinen Rest: ein Protokoll wandert in
# Fehlerberichte und Sicherungen.
_LOGGER.info(
"Token für %s %s", eintrag["titel"], "eingetragen" if token else "gelöscht"
)
await k.app_version_veroeffentlichen()