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:
@@ -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"}
|
||||
Binary file not shown.
@@ -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 & 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()
|
||||
Reference in New Issue
Block a user