Regler stellt den Schlaf-Timeout des Dongles (2026.9.3.10)
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>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||||
|
|
||||||
**Last updated: 2026-09-03** (Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
|
**Last updated: 2026-09-03** (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
|
||||||
zurueck, `2026.9.3.8`, Abschnitt BY. Davor: „Fahrt beenden“ auf Knopfdruck mit vorlaeufigem Ende, Schieberegler in
|
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.
|
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,
|
Davor: Die Fahrt endet wieder bei uns statt im Dongle - Zuendung als Ausloeser,
|
||||||
@@ -9002,3 +9003,112 @@ Passwortfeld mit passendem Platzhalter und die Knöpfe Speichern/Löschen. Einen
|
|||||||
gesetzt → `gesetzt · …0000`, und **der Wert selbst taucht in den veröffentlichten Attributen
|
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
|
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.
|
Sicherheitsabfrage „Token löschen?" entfernt → wieder `nicht gesetzt`, der Gitea-Token unberührt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BZ. Der Regler stellt den Schlaf-Timeout des Dongles (2026.9.3.10)
|
||||||
|
|
||||||
|
### Was der Regler jetzt bedeutet
|
||||||
|
|
||||||
|
„Fahrt beenden" in den Einstellungen ist **eine** Zahl für zwei Dinge, die
|
||||||
|
denselben Sachverhalt beschreiben (Vorgabe des Eigentümers):
|
||||||
|
|
||||||
|
* Schlaf-Timeout des FMM003 (`103`, Minuten). So lange bleibt der Dongle nach
|
||||||
|
dem Abstellen wach und wartet darauf, dass es gleich weitergeht. Danach
|
||||||
|
schläft er — das spart Strom, kostet beim Fortsetzen aber Sekunden fürs
|
||||||
|
Aufwachen.
|
||||||
|
* Unsere eigene Wartezeit, bevor eine Fahrt geschlossen wird.
|
||||||
|
|
||||||
|
Die 180 s `Ignition OFF Delay` (`400`) laufen davor. **Der Nutzer sieht sie
|
||||||
|
nicht** — sie werden nur von der Fahrtdauer abgezogen
|
||||||
|
(`fahrterkennung.ZUENDUNG_NACHLAUF_S`, unverändert).
|
||||||
|
|
||||||
|
Der Regler geht deshalb von **1 bis 60 in Einerschritten** statt 0–60 in
|
||||||
|
Fünferschritten: das Gerät kennt kein Timeout 0 (Minimum 1, Maximum 3000), und
|
||||||
|
„sofort" gibt es damit nicht mehr.
|
||||||
|
|
||||||
|
### Die gemessene API — drei Annahmen waren falsch
|
||||||
|
|
||||||
|
Alles im laufenden Container gegen den echten Account geprüft, nicht aus der
|
||||||
|
Dokumentation abgeschrieben:
|
||||||
|
|
||||||
|
| angenommen | tatsächlich |
|
||||||
|
|---|---|
|
||||||
|
| Wert unter `value` | Wert unter **`current`**, Ausstehendes unter **`pending`** |
|
||||||
|
| Einstellung `sleep_settings` mit `{mode, timeout}` | **`sleep_mode`**, Wert eine Ebene tiefer unter `mode` |
|
||||||
|
| PUT-Rumpf = der Wert | `{"properties": …, "address": …}` — ohne `address` kommt `400 the following properties are missing: address` |
|
||||||
|
|
||||||
|
`GET /gw/devices/{id}/settings/all` liefert die gefüllten Einträge; der Abruf
|
||||||
|
einer **einzelnen** Einstellung kam leer zurück (`current: null`). Gelesen wird
|
||||||
|
deshalb immer `all`.
|
||||||
|
|
||||||
|
Der `Ignition OFF Delay` (`400`) taucht unter keinem sicher zuzuordnenden Namen
|
||||||
|
auf und wird deshalb **nicht** angezeigt — lieber eine Zeile weniger als eine
|
||||||
|
falsch beschriftete. Verglichen werden `103` (gegen den Regler) und `11804`
|
||||||
|
(gegen `NACHLAUF_S`); dazu angezeigt: `102`, `11803`, `11806`.
|
||||||
|
|
||||||
|
### Die eigene Warteschlange ist wieder entfallen
|
||||||
|
|
||||||
|
Erst war ein Auftragsbuch im Koordinator gebaut, weil „Lesen und Schreiben gehen
|
||||||
|
nur bei aktiver Zündung — ausserhalb schläft der Dongle". **Das gilt für das
|
||||||
|
Gerät, nicht für diese Schnittstelle**, und das ist gemessen: die vollständige
|
||||||
|
Konfiguration kam herein, während das Fahrzeug seit dem Vortag stand.
|
||||||
|
|
||||||
|
flespi ist eine Ebene darüber. Es kennt `current` und `pending`, und
|
||||||
|
`"address": "connection"` heisst: zustellen, sobald sich das Gerät meldet. Ein
|
||||||
|
zweites Auftragsbuch bei uns wäre eine zweite Warteschlange für dieselbe
|
||||||
|
Aufgabe — und eine, die nur raten könnte, was die erste schon weiss.
|
||||||
|
|
||||||
|
Entfallen sind damit `flespi_auftrag`, `flespi_auftrag_setzen()`,
|
||||||
|
`wach_geworden()` und der Haken an beiden Zündungs-Beobachtern. Geblieben ist
|
||||||
|
`flespi_stand` (überlebt den Neustart) und `flespi_sperre`.
|
||||||
|
|
||||||
|
**Geschrieben wird jetzt sofort** — nach jedem Profil-Speichern und einmal nach
|
||||||
|
dem Start, über `wunsch_uebernehmen()`. Das ist bewusst zustandsvergleichend
|
||||||
|
statt ereignisgetrieben: es muss auch dann zum Ziel führen, wenn eine frühere
|
||||||
|
Änderung nie ankam. Der billige Vergleich gegen den gespeicherten Stand geht
|
||||||
|
voraus — stimmt er (auch über ein noch ausstehendes `pending`), kostet ein
|
||||||
|
Profil-Speichern **keine einzige Anfrage**. Live belegt: der zweite Neustart
|
||||||
|
schrieb keine Zeile mehr.
|
||||||
|
|
||||||
|
### Gegenlesen heisst hier zweierlei
|
||||||
|
|
||||||
|
Vorgabe des Eigentümers war: nach dem Schreiben lesen und belegen, dass sich
|
||||||
|
genau die eine Stelle geändert hat. Mit `pending` bekommt das eine zweite gültige
|
||||||
|
Ausprägung, und beide zählen als Erfolg:
|
||||||
|
|
||||||
|
* `current` trägt den neuen Wert → das Gerät war wach und hat ihn.
|
||||||
|
* `pending` trägt ihn → flespi hält ihn vor. **Der Normalfall bei stehendem
|
||||||
|
Fahrzeug.**
|
||||||
|
|
||||||
|
Steht er in keinem von beiden, ist der Vorgang nicht angekommen und das wird als
|
||||||
|
Fehler gemeldet. Ändert sich zusätzlich etwas ausserhalb von `sleep_mode`, gibt
|
||||||
|
es eine Warnung — kein Abbruch: geschrieben ist zu dem Zeitpunkt schon.
|
||||||
|
|
||||||
|
### Live nachgewiesen, und was nicht
|
||||||
|
|
||||||
|
Gegen den echten flespi-Account und den echten Dongle (Gerät 8767678):
|
||||||
|
|
||||||
|
* Token sieht **genau ein** Gerät → `geraet_finden()` löst die Nummer selbst
|
||||||
|
auf, in den Optionen muss nichts stehen.
|
||||||
|
* 127 Einstellungen gelesen, **bei parkendem Fahrzeug**.
|
||||||
|
* Regler 15 → PUT akzeptiert → `pending.mode.timeout = 15` bei flespi,
|
||||||
|
`current` blieb 10, und `type`/`bluetooth_on`/`periodic_wakeup` wurden
|
||||||
|
unverändert mitgeschrieben. Genau dafür wird das ganze Objekt
|
||||||
|
zurückgeschrieben statt nur des Feldes.
|
||||||
|
* Zweiter Neustart: keine erneute Anfrage (Idempotenz).
|
||||||
|
|
||||||
|
**Das Gerät hat dabei eine Änderung bekommen** — den Wert, den der Regler ohnehin
|
||||||
|
schon zeigte. Das ist die gewollte Selbstheilung, aber es ist eine Änderung am
|
||||||
|
Fahrzeug und steht deshalb hier.
|
||||||
|
|
||||||
|
**Nicht geprüft:** wie die neue Kachel aussieht. In dieser Sitzung gab es keinen
|
||||||
|
angemeldeten Browser; Panel und App sind über Modul-Parse, `tsc --noEmit` und
|
||||||
|
179/179 Tests belegt, nicht über einen Screenshot. Ebenfalls offen: ob das Gerät
|
||||||
|
das `pending` beim nächsten Verbinden wirklich übernimmt — das zeigt erst die
|
||||||
|
nächste Fahrt.
|
||||||
|
|
||||||
|
**Vorbestehende Lücke, nicht in diesem Zug geschlossen:** `translations/de.json`
|
||||||
|
kennt die zuletzt hinzugekommenen Dienste nicht (`fahrt_jetzt_beenden`,
|
||||||
|
`zugang_setzen`, `csv_importieren`, jetzt auch `dongle_lesen`). Sie erscheinen in
|
||||||
|
den Entwicklerwerkzeugen ohne Klarnamen.
|
||||||
|
|||||||
@@ -351,6 +351,16 @@ export class DataMetricApi {
|
|||||||
return this.rest.dienstAufrufen(DIENST_DOMAIN, "zugang_setzen", { dienst, token });
|
return this.rest.dienstAufrufen(DIENST_DOMAIN, "zugang_setzen", { dienst, token });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Liest die Konfiguration des Dongles - und schreibt dabei eine vorgemerkte
|
||||||
|
Aenderung, falls eine offen ist (siehe flespi.py).
|
||||||
|
|
||||||
|
Wie zugangSetzen bewusst NICHT ueber die Warteschlange: der Abruf gelingt
|
||||||
|
nur bei laufender Zuendung, und Stunden spaeter aus dem Funkloch nachgeholt
|
||||||
|
traefe er das Geraet garantiert im Schlaf an. */
|
||||||
|
dongleLesen(): Promise<unknown> {
|
||||||
|
return this.rest.dienstAufrufen(DIENST_DOMAIN, "dongle_lesen", {});
|
||||||
|
}
|
||||||
|
|
||||||
historieImportieren(start: string, ende: string): Promise<unknown> {
|
historieImportieren(start: string, ende: string): Promise<unknown> {
|
||||||
return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", {
|
return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", {
|
||||||
start,
|
start,
|
||||||
|
|||||||
@@ -234,12 +234,50 @@ export interface ZugangAngabe {
|
|||||||
rest: string | null;
|
rest: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Was am Dongle steht und was noch aussteht (flespi.py).
|
||||||
|
|
||||||
|
`aktiv` ist false, solange kein flespi-Token hinterlegt ist - dann gibt es
|
||||||
|
nichts zu zeigen. `stand` traegt seinen eigenen Zeitstempel: eine Angabe von
|
||||||
|
gestern soll nicht wie eine frische aussehen. Was noch zum Geraet unterwegs
|
||||||
|
ist, steht je Wert in `offen` - flespi haelt es vor, wir nicht. */
|
||||||
|
export interface DongleWert {
|
||||||
|
schluessel: string;
|
||||||
|
/** Die Nummer im flespi-Configurator - danach sucht man dort. */
|
||||||
|
nummer: string;
|
||||||
|
titel: string;
|
||||||
|
einheit: string;
|
||||||
|
geraet: unknown;
|
||||||
|
/** Was noch zum Gerät unterwegs ist (flespis `pending`) - null, wenn nichts
|
||||||
|
aussteht. */
|
||||||
|
offen: unknown;
|
||||||
|
/** Unser eigener Wert, sofern es einen zu vergleichen gibt. */
|
||||||
|
unser: number | null;
|
||||||
|
abweichung: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DongleStand {
|
||||||
|
geraet?: number;
|
||||||
|
gelesen_am: string;
|
||||||
|
anzahl?: number;
|
||||||
|
werte?: DongleWert[];
|
||||||
|
abweichungen?: number;
|
||||||
|
/** Wie viele der gezeigten Werte noch zum Gerät unterwegs sind. */
|
||||||
|
offen?: number;
|
||||||
|
fehler: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DongleAngabe {
|
||||||
|
aktiv: boolean;
|
||||||
|
stand: DongleStand | null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Nutzlast von sensor.audi_dashboard_app_version. */
|
/** Nutzlast von sensor.audi_dashboard_app_version. */
|
||||||
export interface AppVersionAngabe {
|
export interface AppVersionAngabe {
|
||||||
app: string | null;
|
app: string | null;
|
||||||
buendel: Buendelangabe | null;
|
buendel: Buendelangabe | null;
|
||||||
integration_update: IntegrationUpdateAngabe | null;
|
integration_update: IntegrationUpdateAngabe | null;
|
||||||
zugaenge?: ZugangAngabe[];
|
zugaenge?: ZugangAngabe[];
|
||||||
|
dongle?: DongleAngabe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- Entitäts-Verzeichnis */
|
/* -------------------------------------------------- Entitäts-Verzeichnis */
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
type Fahrt,
|
type Fahrt,
|
||||||
type Fahrzeugstatus,
|
type Fahrzeugstatus,
|
||||||
type IntegrationUpdateAngabe,
|
type IntegrationUpdateAngabe,
|
||||||
|
type DongleAngabe,
|
||||||
type ZugangAngabe,
|
type ZugangAngabe,
|
||||||
type Profil,
|
type Profil,
|
||||||
type Tankvorgang,
|
type Tankvorgang,
|
||||||
@@ -71,6 +72,9 @@ export interface DatenWert {
|
|||||||
Integration (aktualisierung.py), oder null vor der ersten Prüfung. */
|
Integration (aktualisierung.py), oder null vor der ersten Prüfung. */
|
||||||
integrationUpdate: IntegrationUpdateAngabe | null
|
integrationUpdate: IntegrationUpdateAngabe | null
|
||||||
zugaenge: ZugangAngabe[]
|
zugaenge: ZugangAngabe[]
|
||||||
|
/** Konfiguration des Dongles und eine ggf. vorgemerkte Aenderung
|
||||||
|
(flespi.py). null, solange kein flespi-Token hinterlegt ist. */
|
||||||
|
dongle: DongleAngabe | null
|
||||||
|
|
||||||
einstellungen: Einstellungen | null
|
einstellungen: Einstellungen | null
|
||||||
fahrzeug: Fahrzeug | null
|
fahrzeug: Fahrzeug | null
|
||||||
@@ -118,6 +122,7 @@ export function DatenAnbieter({
|
|||||||
const [serverVersion, setzeServerVersion] = useState<string | null>(null)
|
const [serverVersion, setzeServerVersion] = useState<string | null>(null)
|
||||||
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
|
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
|
||||||
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
|
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
|
||||||
|
const [dongle, setzeDongle] = useState<DongleAngabe | null>(null)
|
||||||
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
|
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
@@ -162,6 +167,7 @@ export function DatenAnbieter({
|
|||||||
setzeOtaBuendel(versionsangabe?.buendel ?? null)
|
setzeOtaBuendel(versionsangabe?.buendel ?? null)
|
||||||
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
|
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
|
||||||
setzeZugaenge(versionsangabe?.zugaenge ?? [])
|
setzeZugaenge(versionsangabe?.zugaenge ?? [])
|
||||||
|
setzeDongle(versionsangabe?.dongle ?? null)
|
||||||
return true
|
return true
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
|
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
|
||||||
@@ -294,6 +300,7 @@ export function DatenAnbieter({
|
|||||||
otaBuendel,
|
otaBuendel,
|
||||||
integrationUpdate,
|
integrationUpdate,
|
||||||
zugaenge,
|
zugaenge,
|
||||||
|
dongle,
|
||||||
einstellungen,
|
einstellungen,
|
||||||
fahrzeug,
|
fahrzeug,
|
||||||
fahrten,
|
fahrten,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Datumsfeld } from "./Datumsfeld"
|
|||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
|
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
|
||||||
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
|
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
|
||||||
import { datum, datumZeit, isoTag } from "../format"
|
import { datum, datumZeit, isoTag, standAlter } from "../format"
|
||||||
import { useTheme } from "../theme"
|
import { useTheme } from "../theme"
|
||||||
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||||
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
|
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
|
||||||
@@ -43,6 +43,7 @@ export function Einstellungen({
|
|||||||
integrationUpdate,
|
integrationUpdate,
|
||||||
serverVersion,
|
serverVersion,
|
||||||
zugaenge,
|
zugaenge,
|
||||||
|
dongle,
|
||||||
verbindung,
|
verbindung,
|
||||||
api,
|
api,
|
||||||
profilSpeichern,
|
profilSpeichern,
|
||||||
@@ -231,6 +232,28 @@ export function Einstellungen({
|
|||||||
const [tokenLaeuft, setzeTokenLaeuft] = useState<string | null>(null)
|
const [tokenLaeuft, setzeTokenLaeuft] = useState<string | null>(null)
|
||||||
const [tokenFehler, setzeTokenFehler] = useState<string | null>(null)
|
const [tokenFehler, setzeTokenFehler] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Wie tokenLaeuft daneben: der Abruf geht ueber zwei Netze (wir -> flespi ->
|
||||||
|
// 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 [dongleLaeuft, setzeDongleLaeuft] = useState(false)
|
||||||
|
const [dongleFehler, setzeDongleFehler] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const dongleAbrufen = async () => {
|
||||||
|
setzeDongleLaeuft(true)
|
||||||
|
setzeDongleFehler(null)
|
||||||
|
try {
|
||||||
|
await api.dongleLesen()
|
||||||
|
await neuLaden()
|
||||||
|
} catch (fehler) {
|
||||||
|
setzeDongleFehler(
|
||||||
|
fehler instanceof Error ? fehler.message : "Der Abruf ist gescheitert.",
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
setzeDongleLaeuft(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tokenSchreiben = async (dienst: string, wert: string) => {
|
const tokenSchreiben = async (dienst: string, wert: string) => {
|
||||||
setzeTokenLaeuft(dienst)
|
setzeTokenLaeuft(dienst)
|
||||||
setzeTokenFehler(null)
|
setzeTokenFehler(null)
|
||||||
@@ -375,22 +398,61 @@ export function Einstellungen({
|
|||||||
<Tile>
|
<Tile>
|
||||||
<span className="ads-eyebrow">Fahrt beenden</span>
|
<span className="ads-eyebrow">Fahrt beenden</span>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={1}
|
||||||
max={60}
|
max={60}
|
||||||
schritt={5}
|
schritt={1}
|
||||||
wert={werte.pausenzeitMin}
|
wert={werte.pausenzeitMin}
|
||||||
onChange={(min) => aendern({ pausenzeitMin: min })}
|
onChange={(min) => aendern({ pausenzeitMin: min })}
|
||||||
anzeige={werte.pausenzeitMin === 0 ? "sofort" : werte.pausenzeitMin + " Min."}
|
anzeige={werte.pausenzeitMin + " Min."}
|
||||||
aria-label="Minuten bis zum automatischen Fahrtende"
|
aria-label="Minuten bis zum automatischen Fahrtende"
|
||||||
/>
|
/>
|
||||||
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
||||||
Text unter einer Einstellung ist der Footer der Gruppe. */}
|
Text unter einer Einstellung ist der Footer der Gruppe. */}
|
||||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||||
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
|
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
|
||||||
Fahrt automatisch beendet werden soll.
|
Fahrt automatisch beendet werden soll. Derselbe Wert stellt den Schlaf-Timeout
|
||||||
|
des Dongles: so lange bleibt er nach dem Abstellen wach und wartet darauf, dass
|
||||||
|
es gleich weitergeht. Danach schläft er — das spart Strom, kostet beim Fortsetzen
|
||||||
|
aber ein paar Sekunden fürs Aufwachen.
|
||||||
</span>
|
</span>
|
||||||
</Tile>
|
</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 : ""}`}
|
||||||
|
zusatz={
|
||||||
|
w.offen != null
|
||||||
|
? `${w.offen} ist unterwegs zum Gerät`
|
||||||
|
: w.abweichung
|
||||||
|
? `wir rechnen mit ${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
|
||||||
|
? `Gelesen ${standAlter(dongle.stand.gelesen_am)}. `
|
||||||
|
: "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>
|
<Tile>
|
||||||
<span className="ads-eyebrow">Darstellung</span>
|
<span className="ads-eyebrow">Darstellung</span>
|
||||||
<Seg
|
<Seg
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResu
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import selector
|
from homeassistant.helpers import selector
|
||||||
|
|
||||||
from .const import CONF_FLESPI_TOKEN, CONF_GITEA_TOKEN, DOMAIN
|
from .const import CONF_FLESPI_GERAET, CONF_FLESPI_TOKEN, CONF_GITEA_TOKEN, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AudiDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
@@ -82,6 +82,11 @@ class AudiDashboardOptionsFlow(OptionsFlow):
|
|||||||
vol.Optional(CONF_FLESPI_TOKEN, default=""): selector.TextSelector(
|
vol.Optional(CONF_FLESPI_TOKEN, default=""): selector.TextSelector(
|
||||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||||
),
|
),
|
||||||
|
# Leer lassen, solange der Token genau ein Gerät sieht - dann
|
||||||
|
# ermittelt flespi.geraet_finden() die Nummer selbst. Sieht er
|
||||||
|
# mehrere, wird bewusst nicht geraten und die Nummer gehört
|
||||||
|
# hierher.
|
||||||
|
vol.Optional(CONF_FLESPI_GERAET, default=""): selector.TextSelector(),
|
||||||
}),
|
}),
|
||||||
self.config_entry.options,
|
self.config_entry.options,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ D_FAHRT_AKTUALISIEREN = "fahrt_aktualisieren"
|
|||||||
D_FAHRT_LOESCHEN = "fahrt_loeschen"
|
D_FAHRT_LOESCHEN = "fahrt_loeschen"
|
||||||
D_FAHRT_JETZT_BEENDEN = "fahrt_jetzt_beenden"
|
D_FAHRT_JETZT_BEENDEN = "fahrt_jetzt_beenden"
|
||||||
D_ZUGANG_SETZEN = "zugang_setzen"
|
D_ZUGANG_SETZEN = "zugang_setzen"
|
||||||
|
D_DONGLE_LESEN = "dongle_lesen"
|
||||||
D_SCREENING_JETZT = "screening_jetzt"
|
D_SCREENING_JETZT = "screening_jetzt"
|
||||||
D_BELEG_HOCHLADEN = "beleg_hochladen"
|
D_BELEG_HOCHLADEN = "beleg_hochladen"
|
||||||
D_TANKVORGANG_MANUELL = "tankvorgang_manuell"
|
D_TANKVORGANG_MANUELL = "tankvorgang_manuell"
|
||||||
@@ -105,3 +106,6 @@ D_UPDATE_INSTALLIEREN = "update_installieren"
|
|||||||
# Umweg über den Options-Flow importieren können.
|
# Umweg über den Options-Flow importieren können.
|
||||||
CONF_GITEA_TOKEN = "gitea_token"
|
CONF_GITEA_TOKEN = "gitea_token"
|
||||||
CONF_FLESPI_TOKEN = "flespi_token"
|
CONF_FLESPI_TOKEN = "flespi_token"
|
||||||
|
# Die Geräte-Nummer bei flespi. Leer lassen, solange der Token genau ein
|
||||||
|
# Gerät sieht - dann wird sie einmal selbst ermittelt (flespi.geraet_finden).
|
||||||
|
CONF_FLESPI_GERAET = "flespi_geraet"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from . import (
|
|||||||
bilder,
|
bilder,
|
||||||
csv_import,
|
csv_import,
|
||||||
fahrterkennung,
|
fahrterkennung,
|
||||||
|
flespi,
|
||||||
historienimport,
|
historienimport,
|
||||||
identitaet,
|
identitaet,
|
||||||
reifen,
|
reifen,
|
||||||
@@ -53,6 +54,7 @@ from .const import (
|
|||||||
D_ENTITAETEN_SCHREIBEN,
|
D_ENTITAETEN_SCHREIBEN,
|
||||||
D_FAHRT_AKTUALISIEREN,
|
D_FAHRT_AKTUALISIEREN,
|
||||||
D_FAHRT_JETZT_BEENDEN,
|
D_FAHRT_JETZT_BEENDEN,
|
||||||
|
D_DONGLE_LESEN,
|
||||||
D_ZUGANG_SETZEN,
|
D_ZUGANG_SETZEN,
|
||||||
D_FAHRT_LOESCHEN,
|
D_FAHRT_LOESCHEN,
|
||||||
D_FAHRT_MANUELL_ANLEGEN,
|
D_FAHRT_MANUELL_ANLEGEN,
|
||||||
@@ -136,6 +138,7 @@ SCHEMATA: dict[str, vol.Schema] = {
|
|||||||
}),
|
}),
|
||||||
D_FAHRT_LOESCHEN: vol.Schema({vol.Required("trip_id"): cv.string}),
|
D_FAHRT_LOESCHEN: vol.Schema({vol.Required("trip_id"): cv.string}),
|
||||||
D_FAHRT_JETZT_BEENDEN: vol.Schema({}),
|
D_FAHRT_JETZT_BEENDEN: vol.Schema({}),
|
||||||
|
D_DONGLE_LESEN: vol.Schema({}),
|
||||||
D_ZUGANG_SETZEN: vol.Schema({
|
D_ZUGANG_SETZEN: vol.Schema({
|
||||||
vol.Required("dienst"): cv.string,
|
vol.Required("dienst"): cv.string,
|
||||||
# Leerer Text loescht - ein eigener Loesch-Dienst waere ein zweiter Weg
|
# Leerer Text loescht - ein eigener Loesch-Dienst waere ein zweiter Weg
|
||||||
@@ -238,6 +241,30 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
|||||||
await k.ablage.profil_schreiben(_objekt(aufruf.data["profil_json"]))
|
await k.ablage.profil_schreiben(_objekt(aufruf.data["profil_json"]))
|
||||||
await identitaet.fin_ableiten_und_uebernehmen(k.hass, k.zuordnung, k.ablage)
|
await identitaet.fin_ableiten_und_uebernehmen(k.hass, k.zuordnung, k.ablage)
|
||||||
await k.profil_veroeffentlichen()
|
await k.profil_veroeffentlichen()
|
||||||
|
# Die Reglerstellung "Fahrt beenden" ist zugleich der Sollwert für den
|
||||||
|
# Schlaf-Timeout des Dongles (103). Vorgemerkt statt geschrieben:
|
||||||
|
# schreiben geht nur bei wachem Gerät, siehe flespi.py. Muss NACH
|
||||||
|
# profil_veroeffentlichen() stehen - erst dort liest der Koordinator
|
||||||
|
# die neue Reglerstellung mit.
|
||||||
|
await flespi.wunsch_uebernehmen(k)
|
||||||
|
|
||||||
|
async def dongle_lesen(_aufruf: ServiceCall) -> None:
|
||||||
|
"""Liest die Konfiguration des Dongles - auf ausdrücklichen Knopfdruck.
|
||||||
|
|
||||||
|
Nötig ist der Knopf nicht: flespi antwortet auch bei schlafendem Gerät
|
||||||
|
aus seinem eigenen Speicher (siehe flespi.py). Er ist dafür da,
|
||||||
|
nachzusehen, ob eine geschriebene Änderung inzwischen am Gerät
|
||||||
|
angekommen ist.
|
||||||
|
|
||||||
|
Ein Fehler wird als Anzeigestand hinterlegt statt geworfen: er gehört
|
||||||
|
in die Kachel, aus der der Knopf gedrückt wurde, nicht ins Protokoll."""
|
||||||
|
try:
|
||||||
|
await flespi.jetzt_lesen(k)
|
||||||
|
except flespi.FlespiFehler as fehler:
|
||||||
|
_LOGGER.warning("Geräte-Konfiguration nicht gelesen: %s", fehler)
|
||||||
|
await k.flespi_stand_setzen(
|
||||||
|
{"fehler": str(fehler), "gelesen_am": flespi.jetzt_iso()}
|
||||||
|
)
|
||||||
|
|
||||||
async def entitaeten_schreiben(aufruf: ServiceCall) -> None:
|
async def entitaeten_schreiben(aufruf: ServiceCall) -> None:
|
||||||
"""Speichert die im Setup-Menü gewählte Sensor-Zuordnung, wendet sie
|
"""Speichert die im Setup-Menü gewählte Sensor-Zuordnung, wendet sie
|
||||||
@@ -362,6 +389,7 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
|||||||
D_ZUGANG_SETZEN: _als_dienst(
|
D_ZUGANG_SETZEN: _als_dienst(
|
||||||
lambda a: zugaenge.setzen(k, a.data["dienst"], a.data.get("token", ""))
|
lambda a: zugaenge.setzen(k, a.data["dienst"], a.data.get("token", ""))
|
||||||
),
|
),
|
||||||
|
D_DONGLE_LESEN: dongle_lesen,
|
||||||
D_FAHRT_LOESCHEN: _als_dienst(
|
D_FAHRT_LOESCHEN: _als_dienst(
|
||||||
lambda a: fahrterkennung.loeschen(k, a.data["trip_id"])
|
lambda a: fahrterkennung.loeschen(k, a.data["trip_id"])
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -485,18 +485,47 @@ async def _spaetes_aus(
|
|||||||
ende = await _geraetezeit(k, ereigniszeit, jetzt)
|
ende = await _geraetezeit(k, ereigniszeit, jetzt)
|
||||||
echt = ende - datetime.timedelta(seconds=ZUENDUNG_NACHLAUF_S)
|
echt = ende - datetime.timedelta(seconds=ZUENDUNG_NACHLAUF_S)
|
||||||
|
|
||||||
juengste = None
|
# Zwei verschiedene Fragen, deshalb zwei Suchen ueber denselben Bestand:
|
||||||
|
#
|
||||||
|
# juengstes_ende beantwortet "gehoert das Aus zur zuletzt geschlossenen
|
||||||
|
# Fahrt?" - das ist der Verwerfen-Zweig unten.
|
||||||
|
#
|
||||||
|
# kandidat beantwortet "gibt es eine von Hand beendete Fahrt, deren Ende
|
||||||
|
# dieses Aus nachtraegt?". Bewusst NICHT auf die juengste Fahrt beschraenkt:
|
||||||
|
# trifft das gepufferte Aus erst ein, nachdem schon eine weitere Fahrt
|
||||||
|
# aufgezeichnet wurde, waere die vorlaeufige sonst fuer immer zu kurz. Es
|
||||||
|
# gewinnt die vorlaeufige Fahrt, deren Ende dem Aus am naechsten VOR ihm
|
||||||
|
# liegt - jede spaetere kann nicht gemeint sein.
|
||||||
|
fahrten = await k.ablage.fahrten_lesen()
|
||||||
juengstes_ende = None
|
juengstes_ende = None
|
||||||
for fahrt in await k.ablage.fahrten_lesen():
|
kandidat = None
|
||||||
|
kandidat_ende = None
|
||||||
|
for fahrt in fahrten:
|
||||||
roh = fahrt.get("ts_end")
|
roh = fahrt.get("ts_end")
|
||||||
if not roh:
|
if not roh:
|
||||||
continue
|
continue
|
||||||
ts = zeit_normalisiert(roh)
|
ts = zeit_normalisiert(roh)
|
||||||
if juengstes_ende is None or ts > juengstes_ende:
|
if juengstes_ende is None or ts > juengstes_ende:
|
||||||
juengste, juengstes_ende = fahrt, ts
|
juengstes_ende = ts
|
||||||
|
if fahrt.get("ende_vorlaeufig") and ts < echt:
|
||||||
|
if kandidat_ende is None or ts > kandidat_ende:
|
||||||
|
kandidat, kandidat_ende = fahrt, ts
|
||||||
|
|
||||||
if juengste is not None and await _vorlaeufiges_ende_korrigieren(
|
# Eine Fahrt darf nicht ueber eine andere hinweg verlaengert werden. Faengt
|
||||||
k, juengste, juengstes_ende, echt
|
# zwischen dem vorlaeufigen Ende und dem Aus eine Fahrt an, gehoert das Aus
|
||||||
|
# zu ihr oder zu etwas Spaeterem - jedenfalls nicht mehr zur vorlaeufigen,
|
||||||
|
# und zwei sich ueberlappende Fahrten waeren schlimmer als eine zu kurze.
|
||||||
|
if kandidat is not None and _fahrt_dazwischen(fahrten, kandidat, kandidat_ende, echt):
|
||||||
|
_LOGGER.info(
|
||||||
|
"Zuendungs-Aus (Geraetezeit %s) koennte die vorlaeufige Fahrt %s "
|
||||||
|
"verlaengern, aber dazwischen liegt bereits eine andere Fahrt - "
|
||||||
|
"nicht korrigiert",
|
||||||
|
echt.isoformat(), kandidat["trip_id"],
|
||||||
|
)
|
||||||
|
kandidat = None
|
||||||
|
|
||||||
|
if kandidat is not None and await _vorlaeufiges_ende_korrigieren(
|
||||||
|
k, kandidat, kandidat_ende, echt
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -517,6 +546,25 @@ async def _spaetes_aus(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fahrt_dazwischen(
|
||||||
|
fahrten: list[dict],
|
||||||
|
kandidat: dict,
|
||||||
|
kandidat_ende: datetime.datetime,
|
||||||
|
echt: datetime.datetime,
|
||||||
|
) -> bool:
|
||||||
|
"""Faengt zwischen dem vorlaeufigen Ende und dem Aus eine andere Fahrt an?"""
|
||||||
|
for fahrt in fahrten:
|
||||||
|
if fahrt.get("trip_id") == kandidat.get("trip_id"):
|
||||||
|
continue
|
||||||
|
roh = fahrt.get("ts_start")
|
||||||
|
if not roh:
|
||||||
|
continue
|
||||||
|
beginn = zeit_normalisiert(roh)
|
||||||
|
if kandidat_ende < beginn <= echt:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _vorlaeufiges_ende_korrigieren(
|
async def _vorlaeufiges_ende_korrigieren(
|
||||||
k: Koordinator,
|
k: Koordinator,
|
||||||
fahrt: dict,
|
fahrt: dict,
|
||||||
@@ -532,10 +580,17 @@ async def _vorlaeufiges_ende_korrigieren(
|
|||||||
verworfen.
|
verworfen.
|
||||||
|
|
||||||
Drei Bedingungen, jede aus einem eigenen Grund: nur eine als vorlaeufig
|
Drei Bedingungen, jede aus einem eigenen Grund: nur eine als vorlaeufig
|
||||||
gekennzeichnete Fahrt (eine regulaer geschlossene ist bereits richtig),
|
gekennzeichnete Fahrt (eine regulaer geschlossene ist bereits richtig - ihr
|
||||||
nur ein SPAETERES Ende (frueher waere kein Nachtrag, sondern eine
|
Ende IST dieses Signal, ein zweites waere nur die gepufferte Kopie), nur ein
|
||||||
Verkuerzung), und nur innerhalb von KORREKTUR_FENSTER_S (sonst gehoert das
|
SPAETERES Ende (frueher waere kein Nachtrag, sondern eine Verkuerzung), und
|
||||||
Aus wahrscheinlich zu einer ganz anderen Fahrt).
|
nur innerhalb von KORREKTUR_FENSTER_S.
|
||||||
|
|
||||||
|
Zum Fenster: gemessen wird zwischen dem vorlaeufigen Ende und der
|
||||||
|
GERAETEZEIT des Aus - nicht zwischen Knopfdruck und Eintreffen. Ein
|
||||||
|
Datensatz, der eine Woche in der Tiefgarage liegt, traegt trotzdem seinen
|
||||||
|
eigenen Zeitstempel von damals (siehe verlauf.geraetezeit); das Fenster
|
||||||
|
begrenzt also, wie lange nach dem letzten Lebenszeichen noch gefahren
|
||||||
|
worden sein kann, nicht wie lange das Geraet puffern darf.
|
||||||
|
|
||||||
ts_end wandert bewusst NICHT in edited_fields: das Feld ist keine
|
ts_end wandert bewusst NICHT in edited_fields: das Feld ist keine
|
||||||
Handeingabe, und dort eingetragen wuerde es genau diese Korrektur beim
|
Handeingabe, und dort eingetragen wuerde es genau diese Korrektur beim
|
||||||
|
|||||||
@@ -0,0 +1,525 @@
|
|||||||
|
"""Liest und schreibt die Konfiguration des FMM003 über flespi.
|
||||||
|
|
||||||
|
WARUM ÜBERHAUPT
|
||||||
|
---------------
|
||||||
|
Zwei Zahlen dieser Integration gehören eigentlich dem Gerät und stehen bei uns
|
||||||
|
nur als Konstante bzw. Einstellung: `NACHLAUF_S` (Trip: Ignition OFF Timeout)
|
||||||
|
und die Wartezeit bis zum Fahrtende. Ändert jemand die Gegenstücke am Dongle,
|
||||||
|
rechnet die Fahrterkennung still weiter mit dem alten Wert - und niemand merkt
|
||||||
|
es. Genau das ist am 01.09.2026 passiert, als `400` von 0 auf 180 ging: seither
|
||||||
|
endeten alle Fahrten 180 s zu spät, gefunden erst zwei Tage später beim
|
||||||
|
Nachmessen.
|
||||||
|
|
||||||
|
DER REGLER IST DIE EINE STELLE
|
||||||
|
------------------------------
|
||||||
|
"Fahrt beenden" in den Einstellungen ist der Schlaf-Timeout des Dongles
|
||||||
|
(Parameter `103`, in Minuten) - und zugleich unsere eigene Wartezeit, bevor wir
|
||||||
|
eine Fahrt schliessen. Beides ist derselbe Sachverhalt:
|
||||||
|
|
||||||
|
* So lange bleibt der Dongle nach dem Zündungs-Aus wach und wartet darauf, dass
|
||||||
|
es unmittelbar weitergeht. Schläft er erst, dauert das Aufwachen einige
|
||||||
|
Sekunden - bei einem Kaltstart egal, beim Fortsetzen einer Fahrt nicht.
|
||||||
|
* Genau so lange darf für uns eine zurückkehrende Zündung noch dieselbe Fahrt
|
||||||
|
sein.
|
||||||
|
|
||||||
|
Vor dem Timeout laufen zusätzlich die 180 s `Ignition OFF Delay` (`400`) des
|
||||||
|
Geräts. Die muss der Nutzer nicht kennen - sie werden von der Fahrtdauer
|
||||||
|
abgezogen (`fahrterkennung.ZUENDUNG_NACHLAUF_S`), nicht angezeigt.
|
||||||
|
|
||||||
|
Denselben Sachverhalt an zwei Stellen einzustellen ist die Sorte Doppelung, die
|
||||||
|
in diesem Projekt schon mehrfach auseinandergelaufen ist. Der Regler schreibt
|
||||||
|
deshalb beides: unsere Wartezeit und `103`.
|
||||||
|
|
||||||
|
FLESPI PUFFERT SELBST - DESHALB KEINE EIGENE WARTESCHLANGE
|
||||||
|
-----------------------------------------------------------
|
||||||
|
Erst war hier ein eigenes Auftragsbuch gebaut, weil "Lesen und Schreiben gehen
|
||||||
|
nur bei aktiver Zündung" - der Dongle schläft ja. **Das gilt für das Gerät, aber
|
||||||
|
nicht für diese Schnittstelle**, und das ist gemessen, nicht angenommen: die
|
||||||
|
vollständige Konfiguration kam am 03.09.2026 herein, während das Fahrzeug seit
|
||||||
|
dem Vortag stand.
|
||||||
|
|
||||||
|
flespi ist eine Ebene über dem Gerät. Es kennt zu jeder Einstellung `current`
|
||||||
|
(was das Gerät zuletzt gemeldet hat) und `pending` (was noch zu ihm unterwegs
|
||||||
|
ist), und `"address": "connection"` heisst ausdrücklich: zustellen, sobald sich
|
||||||
|
das Gerät das nächste Mal meldet. Ein zweites Auftragsbuch bei uns wäre eine
|
||||||
|
zweite Warteschlange für dieselbe Aufgabe - mit dem zusätzlichen Nachteil, dass
|
||||||
|
sie nur raten könnte, was die erste schon weiss.
|
||||||
|
|
||||||
|
Geschrieben wird deshalb sofort, und was noch aussteht, steht in `pending` und
|
||||||
|
wird so auch angezeigt.
|
||||||
|
|
||||||
|
WAS DIE ANTWORT WIRKLICH ENTHÄLT
|
||||||
|
--------------------------------
|
||||||
|
Auch das ist gemessen, und es hat zwei Annahmen widerlegt (deshalb steht es hier
|
||||||
|
und nicht nur im Code):
|
||||||
|
|
||||||
|
* Der Wert steht in **`current`**, nicht in `value`.
|
||||||
|
* Die Einstellung heisst **`sleep_mode`**, nicht `sleep_settings`, und der Wert
|
||||||
|
liegt eine Ebene tiefer unter `mode`.
|
||||||
|
* Ein PUT braucht `{"properties": ..., "address": ...}` - ohne `address`
|
||||||
|
antwortet flespi mit `400 the following properties are missing: address`.
|
||||||
|
* `GET .../settings/all` liefert die gefüllten Einträge; ein Abruf einer
|
||||||
|
einzelnen Einstellung kam leer zurück. Gelesen wird deshalb immer `all`.
|
||||||
|
|
||||||
|
Der `Ignition OFF Delay` (`400`) taucht unter keinem Namen auf, den wir sicher
|
||||||
|
zuordnen könnten. Er wird deshalb **nicht** angezeigt - lieber eine Zeile
|
||||||
|
weniger als eine falsch beschriftete.
|
||||||
|
|
||||||
|
WAS DER TOKEN DARF
|
||||||
|
------------------
|
||||||
|
Ein flespi-Token lässt sich per ACL begrenzen. Dieser Weg braucht Lesen **und**
|
||||||
|
Schreiben der Geräte-Konfiguration; mehr sollte er nicht dürfen, dann kann auch
|
||||||
|
ein Fehler in diesem Modul nichts anderes am Fahrzeug verändern.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import CONF_FLESPI_GERAET, CONF_FLESPI_TOKEN
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .koordinator import Koordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BASIS = "https://flespi.io"
|
||||||
|
ZEITLIMIT = aiohttp.ClientTimeout(total=20)
|
||||||
|
|
||||||
|
# Die Einstellung, die der Regler schreibt. Der Wert liegt verschachtelt:
|
||||||
|
# current = {"mode": {"timeout": 15, "type": 2, ...}}.
|
||||||
|
SCHLAF_NAME = "sleep_mode"
|
||||||
|
SCHLAF_OBJEKT = "mode"
|
||||||
|
SCHLAF_FELD = "timeout"
|
||||||
|
SCHLAF_SCHLUESSEL = f"{SCHLAF_NAME}.{SCHLAF_OBJEKT}.{SCHLAF_FELD}"
|
||||||
|
|
||||||
|
# Grenzen des Geräts (Angabe des Eigentümers): 1 bis 3000 Minuten. Unser Regler
|
||||||
|
# bleibt deutlich enger; diese Grenzen sind nur der Riegel davor, dass eine 0
|
||||||
|
# oder ein Ausreisser an das Gerät geht.
|
||||||
|
TIMEOUT_MIN = 1
|
||||||
|
TIMEOUT_MAX = 3000
|
||||||
|
|
||||||
|
# "sobald sich das Gerät das nächste Mal meldet" - die Zustellart, die den
|
||||||
|
# schlafenden Dongle überhaupt erreichbar macht. Siehe Modulkopf.
|
||||||
|
ZUSTELLUNG = "connection"
|
||||||
|
|
||||||
|
# Was angezeigt wird, mit der Nummer aus dem Configurator daneben - danach sucht
|
||||||
|
# der Eigentümer dort. Die Pfade sind die gemessenen, nicht die vermuteten.
|
||||||
|
#
|
||||||
|
# "unser" nennt die Konstante dieser Integration, gegen die verglichen wird;
|
||||||
|
# None heisst: nur anzeigen, es gibt nichts zu vergleichen.
|
||||||
|
INTERESSANT: dict[str, dict] = {
|
||||||
|
SCHLAF_SCHLUESSEL: {
|
||||||
|
"nummer": "103",
|
||||||
|
"titel": "Sleep Timeout",
|
||||||
|
"einheit": "Min.",
|
||||||
|
"unser": "regler",
|
||||||
|
},
|
||||||
|
"sleep_mode.mode.type": {
|
||||||
|
"nummer": "102",
|
||||||
|
"titel": "Sleep Mode",
|
||||||
|
"einheit": "",
|
||||||
|
"unser": None,
|
||||||
|
},
|
||||||
|
"trip_scenario.ign_off_timeout": {
|
||||||
|
"nummer": "11804",
|
||||||
|
"titel": "Trip: Ignition OFF Timeout",
|
||||||
|
"einheit": "s",
|
||||||
|
"unser": "NACHLAUF_S",
|
||||||
|
},
|
||||||
|
"trip_scenario.start_speed": {
|
||||||
|
"nummer": "11803",
|
||||||
|
"titel": "Trip: Start Speed",
|
||||||
|
"einheit": "km/h",
|
||||||
|
"unser": None,
|
||||||
|
},
|
||||||
|
"odometer_fmb.source": {
|
||||||
|
"nummer": "11806",
|
||||||
|
"titel": "Odometer-Quelle",
|
||||||
|
"einheit": "",
|
||||||
|
"unser": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FlespiFehler(Exception):
|
||||||
|
"""Der Vorgang ist gescheitert - mit einem Satz, der dem Nutzer etwas sagt."""
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Zugang
|
||||||
|
|
||||||
|
|
||||||
|
def zugang(k: Koordinator) -> tuple[str, int | None] | None:
|
||||||
|
"""Token und Geräte-Nummer, oder None wenn kein Token hinterlegt ist.
|
||||||
|
|
||||||
|
Die Nummer darf fehlen - dann wird sie einmal ermittelt, sofern der Token
|
||||||
|
genau ein Gerät sieht (siehe `geraet_finden`).
|
||||||
|
"""
|
||||||
|
token = (k.entry.options.get(CONF_FLESPI_TOKEN) or "").strip()
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
roh = k.entry.options.get(CONF_FLESPI_GERAET)
|
||||||
|
try:
|
||||||
|
geraet = int(roh) if roh else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
geraet = None
|
||||||
|
return token, geraet
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ HTTP
|
||||||
|
|
||||||
|
|
||||||
|
def _kopf(token: str) -> dict[str, str]:
|
||||||
|
return {"Authorization": f"FlespiToken {token}"}
|
||||||
|
|
||||||
|
|
||||||
|
def _fehlertext(status: int) -> str | None:
|
||||||
|
if status in (401, 403):
|
||||||
|
return (
|
||||||
|
"flespi hat den Token abgelehnt. Er muss die Geräte-Konfiguration "
|
||||||
|
"lesen und schreiben dürfen."
|
||||||
|
)
|
||||||
|
if status == 404:
|
||||||
|
return "flespi kennt dieses Gerät nicht. Stimmt die Geräte-Nummer?"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _anfrage(
|
||||||
|
hass: HomeAssistant, token: str, verb: str, pfad: str, nutzlast: dict | None = None
|
||||||
|
) -> dict:
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
try:
|
||||||
|
async with session.request(
|
||||||
|
verb,
|
||||||
|
f"{BASIS}{pfad}",
|
||||||
|
headers=_kopf(token),
|
||||||
|
json=nutzlast,
|
||||||
|
timeout=ZEITLIMIT,
|
||||||
|
) as antwort:
|
||||||
|
text = _fehlertext(antwort.status)
|
||||||
|
if text:
|
||||||
|
raise FlespiFehler(text)
|
||||||
|
if antwort.status != 200:
|
||||||
|
# flespi nennt im Rumpf den Grund - der ist für die Fehlersuche
|
||||||
|
# mehr wert als die blosse Nummer (so kam die fehlende
|
||||||
|
# "address" heraus, siehe Modulkopf).
|
||||||
|
grund = (await antwort.text())[:200]
|
||||||
|
raise FlespiFehler(f"flespi antwortete mit {antwort.status}: {grund}")
|
||||||
|
return await antwort.json()
|
||||||
|
except asyncio.TimeoutError as fehler:
|
||||||
|
raise FlespiFehler("flespi hat nicht rechtzeitig geantwortet.") from fehler
|
||||||
|
except aiohttp.ClientError as fehler:
|
||||||
|
raise FlespiFehler(f"flespi war nicht erreichbar ({fehler}).") from fehler
|
||||||
|
|
||||||
|
|
||||||
|
async def geraet_finden(hass: HomeAssistant, token: str) -> int:
|
||||||
|
"""Die Geräte-Nummer, wenn der Token genau ein Gerät sieht.
|
||||||
|
|
||||||
|
Sieht er mehrere, wird **nicht** geraten - dann muss die Nummer eingetragen
|
||||||
|
werden. Ein falsch gewähltes Gerät läse und schriebe die Konfiguration
|
||||||
|
eines fremden Trackers, und das fiele niemandem auf.
|
||||||
|
"""
|
||||||
|
daten = await _anfrage(hass, token, "GET", "/gw/devices/all")
|
||||||
|
geraete = daten.get("result") or []
|
||||||
|
if not geraete:
|
||||||
|
raise FlespiFehler("Dieser Token sieht kein einziges Gerät.")
|
||||||
|
if len(geraete) > 1:
|
||||||
|
namen = ", ".join(str(g.get("name") or g.get("id")) for g in geraete[:5])
|
||||||
|
raise FlespiFehler(
|
||||||
|
f"Dieser Token sieht {len(geraete)} Geräte ({namen}) - bitte die "
|
||||||
|
"Geräte-Nummer in den Einstellungen der Integration eintragen."
|
||||||
|
)
|
||||||
|
return int(geraete[0]["id"])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ Antwort aufbereiten
|
||||||
|
|
||||||
|
|
||||||
|
def _flachen(ziel: dict[str, object], praefix: str, wert: object) -> None:
|
||||||
|
"""Verschachtelte Objekte als "a.b.c" ablegen, Blätter als Wert.
|
||||||
|
|
||||||
|
`sleep_mode` bringt seinen Wert eine Ebene tiefer unter `mode`; ohne diese
|
||||||
|
Abflachung wäre der Timeout nicht einzeln ansprechbar.
|
||||||
|
"""
|
||||||
|
ziel[praefix] = wert
|
||||||
|
if isinstance(wert, dict):
|
||||||
|
for name, unter in wert.items():
|
||||||
|
_flachen(ziel, f"{praefix}.{name}", unter)
|
||||||
|
|
||||||
|
|
||||||
|
def _sicht(eintraege: list[dict], feld: str) -> dict[str, object]:
|
||||||
|
"""Alle Einstellungen als "Name -> Wert", gelesen aus `current` bzw. `pending`."""
|
||||||
|
flach: dict[str, object] = {}
|
||||||
|
for eintrag in eintraege:
|
||||||
|
name = eintrag.get("name")
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
wert = eintrag.get(feld)
|
||||||
|
if wert is None:
|
||||||
|
continue
|
||||||
|
_flachen(flach, name, wert)
|
||||||
|
return flach
|
||||||
|
|
||||||
|
|
||||||
|
def _roh(eintraege: list[dict], name: str) -> dict | None:
|
||||||
|
for eintrag in eintraege:
|
||||||
|
if eintrag.get("name") == name:
|
||||||
|
wert = eintrag.get("current")
|
||||||
|
return wert if isinstance(wert, dict) else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def vergleichen(
|
||||||
|
aktuell: dict[str, object],
|
||||||
|
offen: dict[str, object],
|
||||||
|
wunsch: int | None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben."""
|
||||||
|
from . import fahrterkennung
|
||||||
|
|
||||||
|
zeilen = []
|
||||||
|
for schluessel, angabe in INTERESSANT.items():
|
||||||
|
if schluessel not in aktuell and schluessel not in offen:
|
||||||
|
continue
|
||||||
|
geraet = aktuell.get(schluessel)
|
||||||
|
if angabe["unser"] == "regler":
|
||||||
|
unser: object = wunsch
|
||||||
|
elif angabe["unser"]:
|
||||||
|
unser = getattr(fahrterkennung, angabe["unser"], None)
|
||||||
|
else:
|
||||||
|
unser = None
|
||||||
|
# Nur vergleichen, wenn beide Zahlen sind - ein Modus wie "Deep Sleep"
|
||||||
|
# hat kein Gegenstück bei uns.
|
||||||
|
abweichung = (
|
||||||
|
isinstance(geraet, (int, float))
|
||||||
|
and isinstance(unser, (int, float))
|
||||||
|
and int(geraet) != int(unser)
|
||||||
|
)
|
||||||
|
zeilen.append(
|
||||||
|
{
|
||||||
|
"schluessel": schluessel,
|
||||||
|
"nummer": angabe["nummer"],
|
||||||
|
"titel": angabe["titel"],
|
||||||
|
"einheit": angabe["einheit"],
|
||||||
|
"geraet": geraet,
|
||||||
|
# Was noch zum Gerät unterwegs ist - None, wenn nichts aussteht.
|
||||||
|
"offen": offen.get(schluessel),
|
||||||
|
"unser": unser,
|
||||||
|
"abweichung": abweichung,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return zeilen
|
||||||
|
|
||||||
|
|
||||||
|
def jetzt_iso() -> str:
|
||||||
|
"""Jetzt, als ISO-Zeichenkette in UTC - der Zeitstempel jedes Anzeigestands."""
|
||||||
|
return datetime.datetime.now(datetime.UTC).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Lesen
|
||||||
|
|
||||||
|
|
||||||
|
async def _geraet_und_token(k: Koordinator) -> tuple[str, int]:
|
||||||
|
daten = zugang(k)
|
||||||
|
if daten is None:
|
||||||
|
raise FlespiFehler(
|
||||||
|
"Kein flespi-Token hinterlegt - unter Einstellungen → Zugänge eintragen."
|
||||||
|
)
|
||||||
|
token, geraet = daten
|
||||||
|
if not geraet:
|
||||||
|
geraet = await geraet_finden(k.hass, token)
|
||||||
|
return token, geraet
|
||||||
|
|
||||||
|
|
||||||
|
async def _alle_einstellungen(
|
||||||
|
hass: HomeAssistant, token: str, geraet: int
|
||||||
|
) -> list[dict]:
|
||||||
|
daten = await _anfrage(hass, token, "GET", f"/gw/devices/{geraet}/settings/all")
|
||||||
|
eintraege = daten.get("result") or []
|
||||||
|
if not eintraege:
|
||||||
|
raise FlespiFehler("flespi hat keine Einstellungen zu diesem Gerät.")
|
||||||
|
return eintraege
|
||||||
|
|
||||||
|
|
||||||
|
def _stand(geraet: int, eintraege: list[dict], wunsch: int | None) -> dict:
|
||||||
|
aktuell = _sicht(eintraege, "current")
|
||||||
|
offen = _sicht(eintraege, "pending")
|
||||||
|
werte = vergleichen(aktuell, offen, wunsch)
|
||||||
|
return {
|
||||||
|
"geraet": geraet,
|
||||||
|
"gelesen_am": jetzt_iso(),
|
||||||
|
"anzahl": len(eintraege),
|
||||||
|
"werte": werte,
|
||||||
|
"abweichungen": sum(1 for z in werte if z["abweichung"]),
|
||||||
|
"offen": sum(1 for z in werte if z["offen"] is not None),
|
||||||
|
"fehler": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def lesen(k: Koordinator) -> dict:
|
||||||
|
"""Holt die Konfiguration und gibt den fertigen Anzeigestand zurück."""
|
||||||
|
token, geraet = await _geraet_und_token(k)
|
||||||
|
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||||
|
return _stand(geraet, eintraege, wunsch_timeout(k))
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- Schreiben
|
||||||
|
|
||||||
|
|
||||||
|
def wunsch_timeout(k: Koordinator) -> int | None:
|
||||||
|
"""Der Wert, den `103` haben soll - aus der Reglerstellung.
|
||||||
|
|
||||||
|
None, solange das Profil noch nicht gelesen wurde; dann gibt es nichts zu
|
||||||
|
vergleichen und nichts zu schreiben.
|
||||||
|
"""
|
||||||
|
minuten = k.pausenzeit_min
|
||||||
|
if minuten is None:
|
||||||
|
return None
|
||||||
|
return max(TIMEOUT_MIN, min(TIMEOUT_MAX, int(minuten)))
|
||||||
|
|
||||||
|
|
||||||
|
async def schlaf_timeout_schreiben(k: Koordinator, minuten: int) -> dict:
|
||||||
|
"""Setzt `103` am Gerät - lesen, schreiben, gegenlesen.
|
||||||
|
|
||||||
|
Gegengelesen wird, weil eine Änderung am Fahrzeug nur dann vertretbar ist,
|
||||||
|
wenn nachher belegt ist, dass sich genau die eine gewollte Stelle geändert
|
||||||
|
hat (Vorgabe des Eigentümers). "Geändert" heisst hier zweierlei, und beides
|
||||||
|
zählt als Erfolg:
|
||||||
|
|
||||||
|
* `current` trägt den neuen Wert - das Gerät war wach und hat ihn schon.
|
||||||
|
* `pending` trägt ihn - flespi hält ihn vor und stellt ihn zu, sobald sich
|
||||||
|
das Gerät meldet. Das ist der Normalfall bei stehendem Fahrzeug.
|
||||||
|
|
||||||
|
Steht er hinterher in keinem von beiden, ist der Schreibvorgang nicht
|
||||||
|
angekommen und das wird als Fehler gemeldet, nicht als Erfolg.
|
||||||
|
"""
|
||||||
|
minuten = max(TIMEOUT_MIN, min(TIMEOUT_MAX, int(minuten)))
|
||||||
|
token, geraet = await _geraet_und_token(k)
|
||||||
|
|
||||||
|
vorher_roh = await _alle_einstellungen(k.hass, token, geraet)
|
||||||
|
vorher = _sicht(vorher_roh, "current")
|
||||||
|
offen_vorher = _sicht(vorher_roh, "pending")
|
||||||
|
if (
|
||||||
|
vorher.get(SCHLAF_SCHLUESSEL) == minuten
|
||||||
|
and offen_vorher.get(SCHLAF_SCHLUESSEL) is None
|
||||||
|
):
|
||||||
|
_LOGGER.debug("Sleep Timeout steht bereits auf %s Min. - nichts zu tun", minuten)
|
||||||
|
return _stand(geraet, vorher_roh, wunsch_timeout(k))
|
||||||
|
|
||||||
|
# Das ganze Objekt zurückschreiben, nur mit geändertem Feld: `type` und die
|
||||||
|
# übrigen Felder gehören dazu und dürfen beim Schreiben nicht verlorengehen.
|
||||||
|
alt = (_roh(vorher_roh, SCHLAF_NAME) or {}).get(SCHLAF_OBJEKT)
|
||||||
|
neu = (
|
||||||
|
{**alt, SCHLAF_FELD: minuten}
|
||||||
|
if isinstance(alt, dict)
|
||||||
|
else {SCHLAF_FELD: minuten}
|
||||||
|
)
|
||||||
|
await _anfrage(
|
||||||
|
k.hass,
|
||||||
|
token,
|
||||||
|
"PUT",
|
||||||
|
f"/gw/devices/{geraet}/settings/{SCHLAF_NAME}",
|
||||||
|
{"properties": {SCHLAF_OBJEKT: neu}, "address": ZUSTELLUNG},
|
||||||
|
)
|
||||||
|
|
||||||
|
nachher_roh = await _alle_einstellungen(k.hass, token, geraet)
|
||||||
|
nachher = _sicht(nachher_roh, "current")
|
||||||
|
offen = _sicht(nachher_roh, "pending")
|
||||||
|
|
||||||
|
if nachher.get(SCHLAF_SCHLUESSEL) == minuten:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Sleep Timeout (103) am Gerät %s auf %s Min. gesetzt - das Gerät hat ihn",
|
||||||
|
geraet,
|
||||||
|
minuten,
|
||||||
|
)
|
||||||
|
elif offen.get(SCHLAF_SCHLUESSEL) == minuten:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Sleep Timeout (103) auf %s Min. bei flespi hinterlegt - das Gerät "
|
||||||
|
"bekommt ihn, sobald es sich meldet",
|
||||||
|
minuten,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise FlespiFehler(
|
||||||
|
"Der Schreibvorgang ist nicht angekommen: das Gerät steht weiter auf "
|
||||||
|
f"{vorher.get(SCHLAF_SCHLUESSEL)} und es liegt nichts bereit."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nur melden, nicht abbrechen: das Schreiben ist zu diesem Zeitpunkt
|
||||||
|
# geschehen, und ein Abbruch würde daran nichts mehr ändern - eine Warnung
|
||||||
|
# dagegen führt zu der Stelle, an der jemand nachsehen muss.
|
||||||
|
fremd = [
|
||||||
|
s
|
||||||
|
for s in sorted(set(vorher) | set(nachher))
|
||||||
|
if vorher.get(s) != nachher.get(s) and not s.startswith(SCHLAF_NAME)
|
||||||
|
]
|
||||||
|
if fremd:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Am Gerät hat sich mehr geändert als gewollt: %s", ", ".join(fremd[:5])
|
||||||
|
)
|
||||||
|
|
||||||
|
return _stand(geraet, nachher_roh, wunsch_timeout(k))
|
||||||
|
|
||||||
|
|
||||||
|
async def wunsch_uebernehmen(k: Koordinator) -> None:
|
||||||
|
"""Bringt `103` auf die Reglerstellung, wenn beide auseinanderliegen.
|
||||||
|
|
||||||
|
Wird nach jedem Profil-Schreiben und einmal nach dem Start aufgerufen und
|
||||||
|
ist deshalb bewusst zustandsvergleichend statt ereignisgetrieben: sie muss
|
||||||
|
auch dann zum Ziel führen, wenn eine frühere Änderung nie ankam (flespi
|
||||||
|
nicht erreichbar, Neustart mittendrin).
|
||||||
|
|
||||||
|
Ohne hinterlegten Token passiert gar nichts - dann ist der Regler wie bisher
|
||||||
|
nur unsere eigene Wartezeit.
|
||||||
|
"""
|
||||||
|
if zugang(k) is None:
|
||||||
|
return
|
||||||
|
ziel = wunsch_timeout(k)
|
||||||
|
if ziel is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Erst der billige Vergleich gegen den zuletzt gelesenen Stand: stimmt er
|
||||||
|
# schon, kostet das Speichern eines Profils keine einzige Anfrage. Ein noch
|
||||||
|
# ausstehender Wert (`offen`) zählt dabei wie ein gesetzter - er ist schon
|
||||||
|
# unterwegs.
|
||||||
|
if k.flespi_stand:
|
||||||
|
for zeile in k.flespi_stand.get("werte", []):
|
||||||
|
if zeile["schluessel"] != SCHLAF_SCHLUESSEL:
|
||||||
|
continue
|
||||||
|
steht = zeile["offen"] if zeile["offen"] is not None else zeile["geraet"]
|
||||||
|
if isinstance(steht, (int, float)) and int(steht) == ziel:
|
||||||
|
return
|
||||||
|
|
||||||
|
if k.flespi_sperre.locked():
|
||||||
|
return
|
||||||
|
async with k.flespi_sperre:
|
||||||
|
try:
|
||||||
|
stand = await schlaf_timeout_schreiben(k, ziel)
|
||||||
|
except FlespiFehler as fehler:
|
||||||
|
_LOGGER.warning("Sleep Timeout konnte nicht gesetzt werden: %s", fehler)
|
||||||
|
await k.flespi_stand_setzen(
|
||||||
|
{"fehler": str(fehler), "gelesen_am": jetzt_iso()}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await k.flespi_stand_setzen(stand)
|
||||||
|
|
||||||
|
|
||||||
|
async def jetzt_lesen(k: Koordinator) -> None:
|
||||||
|
"""Der Knopf "Jetzt lesen": Stand holen und hinterlegen.
|
||||||
|
|
||||||
|
Fehler fliegen weiter - hier hat jemand gedrückt und wartet auf eine
|
||||||
|
Antwort, auch auf ein "ging nicht".
|
||||||
|
"""
|
||||||
|
async with k.flespi_sperre:
|
||||||
|
stand = await lesen(k)
|
||||||
|
await k.flespi_stand_setzen(stand)
|
||||||
|
# Ein frischer Stand kann eine Abweichung erst sichtbar machen - dann gehört
|
||||||
|
# sie gleich behoben, statt bis zum nächsten Speichern zu warten.
|
||||||
|
await wunsch_uebernehmen(k)
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":"2026.9.3.8","sha256":"f1d22bf45afb23685ed3c0a660b4ef5908e0194f19a1461af6ae928d0163ecdc","bytes":268607,"gebaut":"2026-09-03T13:06:29Z"}
|
{"version":"2026.9.3.10","sha256":"f1a9a5c2ff1ef7fbd29c679e1b1a374d77c57da4f49f864b3758219483bbb013","bytes":269156,"gebaut":"2026-09-03T17:59:17Z"}
|
||||||
Binary file not shown.
@@ -1834,9 +1834,9 @@ function terminText(t) {
|
|||||||
Updates vom Backend, siehe datenLaden()) selbständig hoch und schaltet
|
Updates vom Backend, siehe datenLaden()) selbständig hoch und schaltet
|
||||||
dabei die Einheit um - Sek. bis 60, dann Min. bis 60, dann Std. bis 24,
|
dabei die Einheit um - Sek. bis 60, dann Min. bis 60, dann Std. bis 24,
|
||||||
dann Tage. */
|
dann Tage. */
|
||||||
function standAlterText() {
|
function alterText(ts) {
|
||||||
if (LETZTES_UPDATE_TS == null) return "unbekannt";
|
if (ts == null || !Number.isFinite(ts)) return "unbekannt";
|
||||||
const sekunden = Math.max(0, Math.floor((Date.now() - LETZTES_UPDATE_TS) / 1000));
|
const sekunden = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
||||||
if (sekunden < 60) return `vor ${sekunden} Sek.`;
|
if (sekunden < 60) return `vor ${sekunden} Sek.`;
|
||||||
const minuten = Math.floor(sekunden / 60);
|
const minuten = Math.floor(sekunden / 60);
|
||||||
if (minuten < 60) return `vor ${minuten} Min.`;
|
if (minuten < 60) return `vor ${minuten} Min.`;
|
||||||
@@ -1844,6 +1844,9 @@ function standAlterText() {
|
|||||||
if (stunden < 24) return `vor ${stunden} Std.`;
|
if (stunden < 24) return `vor ${stunden} Std.`;
|
||||||
return `vor ${Math.floor(stunden / 24)} Tage`;
|
return `vor ${Math.floor(stunden / 24)} Tage`;
|
||||||
}
|
}
|
||||||
|
function standAlterText() {
|
||||||
|
return alterText(LETZTES_UPDATE_TS);
|
||||||
|
}
|
||||||
function standAlterTicken() {
|
function standAlterTicken() {
|
||||||
if (!ROOT || !CAR) return;
|
if (!ROOT || !CAR) return;
|
||||||
const sy = $("#sync span"); if (sy) sy.textContent = standAlterText();
|
const sy = $("#sync span"); if (sy) sy.textContent = standAlterText();
|
||||||
@@ -3100,16 +3103,47 @@ function vEinst() {
|
|||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
<div class="tile"><span class="label">Fahrt beenden</span>
|
<div class="tile"><span class="label">Fahrt beenden</span>
|
||||||
<div class="schieber" style="margin-top:14px">
|
<div class="schieber" style="margin-top:14px">
|
||||||
<input type="range" min="0" max="60" step="5" value="${CONFIG.pausenzeit}"
|
<input type="range" min="1" max="60" step="1" value="${CONFIG.pausenzeit}"
|
||||||
style="--fuell:${(CONFIG.pausenzeit / 60) * 100}%" data-pausenzeit
|
style="--fuell:${((CONFIG.pausenzeit - 1) / 59) * 100}%" data-pausenzeit
|
||||||
aria-label="Minuten bis zum automatischen Fahrtende">
|
aria-label="Minuten bis zum automatischen Fahrtende">
|
||||||
<output>${CONFIG.pausenzeit === 0 ? "sofort" : CONFIG.pausenzeit + " Min."}</output>
|
<output>${CONFIG.pausenzeit} Min.</output>
|
||||||
</div>
|
</div>
|
||||||
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
|
||||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
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
|
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
|
||||||
Fahrt automatisch beendet werden soll.</span>
|
Fahrt automatisch beendet werden soll. Derselbe Wert stellt den Schlaf-Timeout
|
||||||
|
des Dongles: so lange bleibt er nach dem Abstellen wach und wartet darauf,
|
||||||
|
dass es gleich weitergeht. Danach schläft er — das spart Strom, kostet beim
|
||||||
|
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
|
||||||
</div>
|
</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
|
||||||
|
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)))}. `
|
||||||
|
: "Noch nicht gelesen. "}Lesen und Schreiben gehen nur bei laufender Zündung —
|
||||||
|
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="tile"><span class="label">Darstellung</span>
|
||||||
<div class="seg">
|
<div class="seg">
|
||||||
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
|
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
|
||||||
@@ -4158,6 +4192,13 @@ let INTEGRATION_UPDATE = null;
|
|||||||
/* Nur "gesetzt/nicht gesetzt" je Dienst - der Token selbst kommt nie ueber
|
/* Nur "gesetzt/nicht gesetzt" je Dienst - der Token selbst kommt nie ueber
|
||||||
die Leitung, siehe zugaenge.py im Backend. */
|
die Leitung, siehe zugaenge.py im Backend. */
|
||||||
let ZUGAENGE = [];
|
let ZUGAENGE = [];
|
||||||
|
/* Was am Dongle steht, was noch aussteht - siehe flespi.py im Backend.
|
||||||
|
null, solange kein flespi-Token hinterlegt ist. */
|
||||||
|
let DONGLE = 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.
|
||||||
|
let DONGLE_LAEUFT = false;
|
||||||
// "pruefen" | "installieren" | "neustart" | null - während einer dieser drei
|
// "pruefen" | "installieren" | "neustart" | null - während einer dieser drei
|
||||||
// Aktionen läuft, siehe vEinst()'s Integration-Update-Kachel. Eigens statt
|
// Aktionen läuft, siehe vEinst()'s Integration-Update-Kachel. Eigens statt
|
||||||
// über serviceRufen() (das feuert-und-vergisst), weil diese drei Aktionen
|
// über serviceRufen() (das feuert-und-vergisst), weil diese drei Aktionen
|
||||||
@@ -6001,6 +6042,18 @@ function ereignisseVerdrahten() {
|
|||||||
// div ohne data-go, aber die Reihenfolge haelt das auch dann, wenn die
|
// div ohne data-go, aber die Reihenfolge haelt das auch dann, wenn die
|
||||||
// Kachel spaeter einmal anders gebaut wird.
|
// Kachel spaeter einmal anders gebaut wird.
|
||||||
if (e.target.closest("[data-fahrtbeenden]")) { serviceRufen("fahrt_jetzt_beenden", {}); return; }
|
if (e.target.closest("[data-fahrtbeenden]")) { serviceRufen("fahrt_jetzt_beenden", {}); return; }
|
||||||
|
if (e.target.closest("[data-dongle]")) {
|
||||||
|
// Nicht ueber serviceRufen(): das feuert und vergisst, hier soll der Knopf
|
||||||
|
// aber zeigen, dass gerade zwei Netze befragt werden - und danach wieder
|
||||||
|
// freigeben. Das Ergebnis (auch ein Fehler) kommt ueber die veroeffentlichte
|
||||||
|
// Entitaet zurueck, siehe flespi.py; deshalb hier nur der Ladezustand.
|
||||||
|
if (DONGLE_LAEUFT) return;
|
||||||
|
DONGLE_LAEUFT = true; render();
|
||||||
|
HASS.callService(DOMAIN, "dongle_lesen", {})
|
||||||
|
.catch((f) => console.error("dongle_lesen", f))
|
||||||
|
.finally(() => { DONGLE_LAEUFT = false; render(); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const tokSpeichern = e.target.closest("[data-tokenspeichern]");
|
const tokSpeichern = e.target.closest("[data-tokenspeichern]");
|
||||||
if (tokSpeichern) {
|
if (tokSpeichern) {
|
||||||
const dienst = tokSpeichern.dataset.tokenspeichern;
|
const dienst = tokSpeichern.dataset.tokenspeichern;
|
||||||
@@ -6391,6 +6444,9 @@ async function datenLaden(erzwingen) {
|
|||||||
ZUGAENGE = versionState.attributes.daten
|
ZUGAENGE = versionState.attributes.daten
|
||||||
? versionState.attributes.daten.zugaenge ?? []
|
? versionState.attributes.daten.zugaenge ?? []
|
||||||
: [];
|
: [];
|
||||||
|
DONGLE = versionState.attributes.daten
|
||||||
|
? versionState.attributes.daten.dongle ?? null
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
|
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from homeassistant.helpers.storage import Store
|
|||||||
from . import (
|
from . import (
|
||||||
batterie,
|
batterie,
|
||||||
fahrterkennung,
|
fahrterkennung,
|
||||||
|
flespi,
|
||||||
identitaet,
|
identitaet,
|
||||||
reifen,
|
reifen,
|
||||||
screening,
|
screening,
|
||||||
@@ -145,6 +146,20 @@ class Koordinator:
|
|||||||
# liest diesen Wert bei jedem Takt erneut mit, damit er nicht
|
# liest diesen Wert bei jedem Takt erneut mit, damit er nicht
|
||||||
# zwischen zwei Prüfungen verschwindet.
|
# zwischen zwei Prüfungen verschwindet.
|
||||||
self.letzte_update_pruefung: dict | None = None
|
self.letzte_update_pruefung: dict | None = None
|
||||||
|
# Die Reglerstellung "Fahrt beenden" in Minuten, aus dem Profil
|
||||||
|
# mitgelesen. Sie ist zugleich der Sollwert fuer den Schlaf-Timeout
|
||||||
|
# des Dongles (103) - siehe flespi.py. None = noch nicht gelesen.
|
||||||
|
self.pausenzeit_min: int | None = None
|
||||||
|
# Der zuletzt gelesene Stand der Geraete-Konfiguration. Ueberlebt einen
|
||||||
|
# Neustart, damit die Anzeige nicht auf "unbekannt" zurueckfaellt,
|
||||||
|
# obwohl sie erst eine Stunde alt ist - und damit wunsch_uebernehmen()
|
||||||
|
# ohne Netzabfrage weiss, ob ueberhaupt etwas zu tun ist. Was noch zum
|
||||||
|
# Geraet unterwegs ist, steht darin als "offen" (siehe flespi.py);
|
||||||
|
# eine eigene Warteschlange gibt es bewusst nicht.
|
||||||
|
self.flespi_stand: dict | None = None
|
||||||
|
# Zwei Wege koennen gleichzeitig schreiben wollen (Profil gespeichert,
|
||||||
|
# Knopf gedrueckt) - der zweite haette nichts mehr zu tun.
|
||||||
|
self.flespi_sperre = asyncio.Lock()
|
||||||
|
|
||||||
self._takte: list[CALLBACK_TYPE] = []
|
self._takte: list[CALLBACK_TYPE] = []
|
||||||
self._beobachter: list[CALLBACK_TYPE] = []
|
self._beobachter: list[CALLBACK_TYPE] = []
|
||||||
@@ -198,6 +213,10 @@ class Koordinator:
|
|||||||
# Fahrt warten. Reicht das Ortsbudget eines Laufs nicht, plant das
|
# Fahrt warten. Reicht das Ortsbudget eines Laufs nicht, plant das
|
||||||
# Screening selbst einen zweiten ein (screening._nachfassen_planen).
|
# Screening selbst einen zweiten ein (screening._nachfassen_planen).
|
||||||
await screening.durchfuehren(self)
|
await screening.durchfuehren(self)
|
||||||
|
# Weicht die Reglerstellung vom zuletzt gelesenen Geraetewert ab,
|
||||||
|
# wird die Aenderung hier vorgemerkt - auch wenn sie waehrend eines
|
||||||
|
# Ausfalls gesetzt wurde oder ein frueherer Auftrag nie zum Zug kam.
|
||||||
|
await flespi.wunsch_uebernehmen(self)
|
||||||
|
|
||||||
async def beenden(self) -> None:
|
async def beenden(self) -> None:
|
||||||
if self.screening_nachlauf is not None:
|
if self.screening_nachlauf is not None:
|
||||||
@@ -385,6 +404,7 @@ class Koordinator:
|
|||||||
)
|
)
|
||||||
geladen = None
|
geladen = None
|
||||||
self.geparkt_seit_ts = geladen
|
self.geparkt_seit_ts = geladen
|
||||||
|
self.flespi_stand = gespeichert.get("flespi_stand")
|
||||||
|
|
||||||
async def _laufzeit_sichern(self) -> None:
|
async def _laufzeit_sichern(self) -> None:
|
||||||
await self._store.async_save({
|
await self._store.async_save({
|
||||||
@@ -394,8 +414,14 @@ class Koordinator:
|
|||||||
"geparkt_seit_ts": (
|
"geparkt_seit_ts": (
|
||||||
self.geparkt_seit_ts.isoformat() if self.geparkt_seit_ts else None
|
self.geparkt_seit_ts.isoformat() if self.geparkt_seit_ts else None
|
||||||
),
|
),
|
||||||
|
"flespi_stand": self.flespi_stand,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async def flespi_stand_setzen(self, stand: dict | None) -> None:
|
||||||
|
self.flespi_stand = stand
|
||||||
|
await self._laufzeit_sichern()
|
||||||
|
await self.app_version_veroeffentlichen()
|
||||||
|
|
||||||
async def fahrt_start_setzen(self, ts: datetime.datetime | None) -> None:
|
async def fahrt_start_setzen(self, ts: datetime.datetime | None) -> None:
|
||||||
self.fahrt_start_ts = ts
|
self.fahrt_start_ts = ts
|
||||||
await self._laufzeit_sichern()
|
await self._laufzeit_sichern()
|
||||||
@@ -501,6 +527,15 @@ class Koordinator:
|
|||||||
if profil is None:
|
if profil is None:
|
||||||
return
|
return
|
||||||
self.setzen(E_PROFIL, "aktuell", profil)
|
self.setzen(E_PROFIL, "aktuell", profil)
|
||||||
|
# Die Reglerstellung mitlesen: sie ist der Sollwert für den
|
||||||
|
# Schlaf-Timeout des Dongles, und flespi.py hat keinen eigenen
|
||||||
|
# Zugriff auf das Profil.
|
||||||
|
try:
|
||||||
|
self.pausenzeit_min = int(
|
||||||
|
(profil.get("einstellungen") or {})["fahrten_pausenzeit_min"]
|
||||||
|
)
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
self.pausenzeit_min = None
|
||||||
# Die drei Reifen-Kontrollwerte hängen am selben Profil - zusammen
|
# Die drei Reifen-Kontrollwerte hängen am selben Profil - zusammen
|
||||||
# veröffentlichen, damit sie nicht auseinanderlaufen können.
|
# veröffentlichen, damit sie nicht auseinanderlaufen können.
|
||||||
# Die beiden km-Stände bleiben Zahlen (die Entitäten tragen die
|
# Die beiden km-Stände bleiben Zahlen (die Entitäten tragen die
|
||||||
@@ -595,6 +630,12 @@ class Koordinator:
|
|||||||
# der Token selbst verlaesst die Integration nie, siehe
|
# der Token selbst verlaesst die Integration nie, siehe
|
||||||
# zugaenge.py.
|
# zugaenge.py.
|
||||||
"zugaenge": zugaenge.stand(self),
|
"zugaenge": zugaenge.stand(self),
|
||||||
|
# Was am Dongle steht, was noch aussteht, und wie alt die
|
||||||
|
# Angabe ist - siehe flespi.py.
|
||||||
|
"dongle": {
|
||||||
|
"aktiv": flespi.zugang(self) is not None,
|
||||||
|
"stand": self.flespi_stand,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "Audi Dashboard",
|
"name": "Audi Dashboard",
|
||||||
"version": "2026.9.3.8",
|
"version": "2026.9.3.10",
|
||||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": ["@paul"],
|
"codeowners": ["@paul"],
|
||||||
|
|||||||
@@ -345,3 +345,6 @@ zugang_setzen:
|
|||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
dongle_lesen:
|
||||||
|
fields: {}
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ DIENSTE: dict[str, dict] = {
|
|||||||
"flespi": {
|
"flespi": {
|
||||||
"schluessel": CONF_FLESPI_TOKEN,
|
"schluessel": CONF_FLESPI_TOKEN,
|
||||||
"titel": "flespi",
|
"titel": "flespi",
|
||||||
"zweck": "Liest die Konfiguration des Dongles (noch nicht in Gebrauch).",
|
"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,
|
"erforderlich": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,9 +220,10 @@ class Nachziehen(Basis):
|
|||||||
)
|
)
|
||||||
self.assertEqual(k.ablage.aktualisiert, [])
|
self.assertEqual(k.ablage.aktualisiert, [])
|
||||||
|
|
||||||
async def test_nur_die_juengste_fahrt_wird_korrigiert(self):
|
async def test_keine_verlaengerung_ueber_eine_andere_fahrt_hinweg(self):
|
||||||
"""Eine aeltere vorlaeufige Fahrt darf ein spaetes Aus nicht an sich
|
"""Faengt zwischen dem vorlaeufigen Ende und dem Aus eine andere Fahrt
|
||||||
ziehen - sonst wanderte ihr Ende ueber die naechste Fahrt hinweg."""
|
an, gehoert das Aus zu ihr - zwei sich ueberlappende Fahrten waeren
|
||||||
|
schlimmer als eine zu kurze."""
|
||||||
alt_start = T0 - datetime.timedelta(hours=3)
|
alt_start = T0 - datetime.timedelta(hours=3)
|
||||||
k = FakeKoordinator(
|
k = FakeKoordinator(
|
||||||
start_ts=None,
|
start_ts=None,
|
||||||
@@ -236,9 +237,29 @@ class Nachziehen(Basis):
|
|||||||
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
k.ablage.aktualisiert, [], "die juengste Fahrt ist regulaer geschlossen"
|
k.ablage.aktualisiert, [], "dazwischen liegt der Beginn von t-neu"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_aeltere_vorlaeufige_fahrt_wird_gefunden(self):
|
||||||
|
"""Die Suche ist NICHT auf die juengste Fahrt beschraenkt: eine aeltere
|
||||||
|
regulaer geschlossene Fahrt darf die Korrektur nicht verdecken."""
|
||||||
|
frueher = T0 - datetime.timedelta(hours=5)
|
||||||
|
k = FakeKoordinator(
|
||||||
|
start_ts=None,
|
||||||
|
fahrten=[
|
||||||
|
fahrt("t-frueh", frueher, frueher + datetime.timedelta(minutes=20), False),
|
||||||
|
fahrt("t-hand", T0, LEBENSZEICHEN, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
echt = LEBENSZEICHEN + datetime.timedelta(minutes=8)
|
||||||
|
await self._spaetes_aus(
|
||||||
|
k, echt + datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S)
|
||||||
|
)
|
||||||
|
self.assertEqual(len(k.ablage.aktualisiert), 1)
|
||||||
|
trip_id, aenderungen = k.ablage.aktualisiert[0]
|
||||||
|
self.assertEqual(trip_id, "t-hand")
|
||||||
|
self.assertEqual(aenderungen["ts_end"], echt.isoformat())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
Reference in New Issue
Block a user