4d4890034a
Der Regler "Fahrt beenden" ist jetzt eine Zahl fuer zwei Dinge: unsere eigene
Wartezeit bis zum Fahrtende und der Schlaf-Timeout des FMM003 (103). So lange
bleibt der Dongle wach und wartet darauf, dass es gleich weitergeht; danach
schlaeft er. Die 180 s Ignition OFF Delay laufen davor und bleiben unsichtbar -
sie werden nur von der Fahrtdauer abgezogen. Der Regler geht deshalb von 1 bis
60 in Einerschritten; das Geraet kennt kein Timeout 0.
flespi.py liest und schreibt die Geraete-Konfiguration. Drei Annahmen sind an
der echten API gefallen und stehen als Messung im Modulkopf: der Wert liegt in
current (nicht value), die Einstellung heisst sleep_mode mit dem Wert eine Ebene
tiefer unter mode, und ein PUT braucht {"properties": ..., "address": ...}.
Die eigene Auftrags-Warteschlange ist wieder entfallen. Sie war fuer "das Geraet
schlaeft" gebaut - das gilt aber nur fuer das Geraet, nicht fuer die
Schnittstelle: die vollstaendige Konfiguration kam herein, waehrend das Fahrzeug
seit dem Vortag stand. flespi puffert selbst (pending + address=connection), ein
zweites Auftragsbuch waere eine zweite Warteschlange fuer dieselbe Aufgabe.
Gegengelesen wird nach jedem Schreiben; als Erfolg zaehlt current ODER pending -
letzteres ist bei stehendem Fahrzeug der Normalfall.
Live gegen den echten Account belegt: Token sieht genau ein Geraet, 127
Einstellungen bei parkendem Fahrzeug gelesen, Regler 15 geschrieben und als
pending bestaetigt (Nachbarfelder unveraendert), zweiter Neustart ohne erneute
Anfrage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
4.8 KiB
Python
126 lines
4.8 KiB
Python
"""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 und schreibt den Schlaf-Timeout. "
|
|
"Der Token braucht dafür Lese- und Schreibrecht auf die "
|
|
"Geräte-Konfiguration."
|
|
),
|
|
"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()
|