diff --git a/AGENTS.md b/AGENTS.md index a86eae8..110f49a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/companion-app/src/api/index.ts b/companion-app/src/api/index.ts index b094adb..3e59869 100644 --- a/companion-app/src/api/index.ts +++ b/companion-app/src/api/index.ts @@ -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 { + return this.rest.dienstAufrufen(DIENST_DOMAIN, "zugang_setzen", { dienst, token }); + } + historieImportieren(start: string, ende: string): Promise { return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", { start, diff --git a/companion-app/src/api/types.ts b/companion-app/src/api/types.ts index 222868c..9e219c0 100644 --- a/companion-app/src/api/types.ts +++ b/companion-app/src/api/types.ts @@ -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 */ diff --git a/companion-app/src/daten/DatenKontext.tsx b/companion-app/src/daten/DatenKontext.tsx index f80184a..830b894 100644 --- a/companion-app/src/daten/DatenKontext.tsx +++ b/companion-app/src/daten/DatenKontext.tsx @@ -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([]) const [serverVersion, setzeServerVersion] = useState(null) const [otaBuendel, setzeOtaBuendel] = useState(null) + const [zugaenge, setzeZugaenge] = useState([]) const [integrationUpdate, setzeIntegrationUpdate] = useState( 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, diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index 4e7741d..65fcefe 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -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>({}) + const [tokenLaeuft, setzeTokenLaeuft] = useState(null) + const [tokenFehler, setzeTokenFehler] = useState(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({ )} + {zugaenge.length > 0 && ( + + Zugänge + {/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben — + man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */} + {zugaenge.map((z) => ( + + {z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"} + + } + > +

{z.zweck}

+ + + setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value })) + } + /> + +
+ void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")} + > + {tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"} + + {z.gesetzt && ( + { + if (bestaetigen(`Token für ${z.titel} löschen?`)) { + void tokenSchreiben(z.dienst, "") + } + }} + > + Löschen + + )} +
+
+ ))} + {tokenFehler &&

{tokenFehler}

} + + Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist. + Dieselben Felder gibt es in Home Assistant unter Einstellungen → Geräte & + Dienste → Audi Dashboard → Konfigurieren. + +
+ )} + Version 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, ), diff --git a/custom_components/audi_dashboard/const.py b/custom_components/audi_dashboard/const.py index 085a012..b39e243 100644 --- a/custom_components/audi_dashboard/const.py +++ b/custom_components/audi_dashboard/const.py @@ -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" diff --git a/custom_components/audi_dashboard/dienste.py b/custom_components/audi_dashboard/dienste.py index 01ab466..aa94e57 100644 --- a/custom_components/audi_dashboard/dienste.py +++ b/custom_components/audi_dashboard/dienste.py @@ -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"]) ), diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index cad141f..4d99edb 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.9.3.7","sha256":"82f5fdbf1ab4281a51f0c2d31f27b7609b4bc88835c19641d071f9db4970bb00","bytes":268073,"gebaut":"2026-09-03T12:57:42Z"} \ No newline at end of file +{"version":"2026.9.3.8","sha256":"f1d22bf45afb23685ed3c0a660b4ef5908e0194f19a1461af6ae928d0163ecdc","bytes":268607,"gebaut":"2026-09-03T13:06:29Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 8b4a9e9..f55de70 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index d634dc1..1185853 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -3076,6 +3076,28 @@ function vEinst() { ` : ""} + ${ZUGAENGE.length ? `
Zugänge + ${ZUGAENGE.map((z, i) => ` + +
+ ${esc(z.zweck)} +
+
+ + ${z.gesetzt ? `` : ""} +
`).join("")} + + Ein gespeicherter Token wird nie wieder angezeigt — nur ob einer hinterlegt ist. + Dieselben Felder gibt es unter Einstellungen → Geräte & Dienste → Audi Dashboard → + Konfigurieren. +
` : ""}
Fahrt beenden
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; diff --git a/custom_components/audi_dashboard/koordinator.py b/custom_components/audi_dashboard/koordinator.py index 49a43bb..10f077e 100644 --- a/custom_components/audi_dashboard/koordinator.py +++ b/custom_components/audi_dashboard/koordinator.py @@ -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), }, ) diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 8497d0d..1328193 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -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"], diff --git a/custom_components/audi_dashboard/services.yaml b/custom_components/audi_dashboard/services.yaml index 2e074b6..8d42781 100644 --- a/custom_components/audi_dashboard/services.yaml +++ b/custom_components/audi_dashboard/services.yaml @@ -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: diff --git a/custom_components/audi_dashboard/zugaenge.py b/custom_components/audi_dashboard/zugaenge.py new file mode 100644 index 0000000..3e49cd6 --- /dev/null +++ b/custom_components/audi_dashboard/zugaenge.py @@ -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()