flespi: echtes Lesen statt Zwischenspeicher, und kein Gleichstand ohne Grundlage
Der Waechter meldete am 04.09.2026 "stimmt ueberein" fuer
trip_scenario.ign_off_timeout: 900 gegen unsere Konstante NACHLAUF_S = 900.
Das Geraet hat dort 0 - das Trip-Szenario ist abgeschaltet. flespis
Zwischenspeicher trug den Stand vom 01.09. 13:15, dreieinhalb Tage alt.
Der Eigentuemer hat es aufgedeckt: "die gelesenen werte sind veraltet".
Zwei Aenderungen:
1. "Jetzt lesen" leert erst den Zwischenspeicher der ueberwachten
Einstellungen (DELETE /gw/devices/{id}/settings/{name} - die
API-Entsprechung des Panel-Knopfes "clear cache and synchronize"; loescht
den gespeicherten Wert, nicht den im Geraet) und holt dann neu.
Einstellungen mit einem AUSSTEHENDEN Wert werden uebersprungen: dasselbe
DELETE wuerde ihn verwerfen, und die Aenderung kaeme nie an - wie
1003/1004, die am 04.09. acht Stunden in der Warteschlange standen.
2. `abweichung` ist dreiwertig: true, false und **null (kein Urteil)**. Null
steht dort, wo ein Vergleich nichts aussagt - der Wert wurde nie vom
Geraet bestaetigt, oder es ist gerade eine Aenderung unterwegs. Dazu
traegt jede Zeile `gemeldet_am` (flespis `updated`), und beide
Oberflaechen zeigen es an: "bestaetigt 01.09.2026 (4 T. alt)" bzw.
"nie bestaetigt".
Ein stiller Gleichstand mit einem veralteten Wert ist schlimmer als gar kein
Vergleich: er behauptet Sicherheit, wo keine ist.
tsc sauber, 271 Tests, vite build sauber, Panel als Modul geparst,
py_compile sauber, audi_ha_test auf 2026.9.4.26 ohne Traceback.
This commit is contained in:
@@ -255,7 +255,15 @@ export interface DongleWert {
|
||||
offen: unknown;
|
||||
/** Unser eigener Wert, sofern es einen zu vergleichen gibt. */
|
||||
unser: number | null;
|
||||
abweichung: boolean;
|
||||
/** Dreiwertig: true weicht ab, false stimmt überein, **null kein
|
||||
Urteil** — der Wert wurde nie vom Gerät bestätigt, oder es ist
|
||||
gerade eine Änderung zu ihm unterwegs. Bis zum 04.09.2026 war das
|
||||
ein Bool, und `false` behauptete Übereinstimmung auch gegen einen
|
||||
tagealten Wert. */
|
||||
abweichung: boolean | null;
|
||||
/** Wann das Gerät diesen Wert zuletzt bestätigt hat (flespis
|
||||
`updated`) — null heißt: nie. */
|
||||
gemeldet_am?: string | null;
|
||||
}
|
||||
|
||||
export interface DongleStand {
|
||||
@@ -264,6 +272,10 @@ export interface DongleStand {
|
||||
anzahl?: number;
|
||||
werte?: DongleWert[];
|
||||
abweichungen?: number;
|
||||
/** Zeilen ohne Urteil: nie bestätigt oder gerade unterwegs. */
|
||||
unbekannt?: number;
|
||||
/** Welche Zwischenspeicher dieser Abruf geleert hat. */
|
||||
geleert?: string[];
|
||||
/** Wie viele der gezeigten Werte noch zum Gerät unterwegs sind. */
|
||||
offen?: number;
|
||||
fehler: string | null;
|
||||
|
||||
@@ -25,6 +25,19 @@ import { HistorienImport } from "./HistorienImport"
|
||||
import type { ServicebuchEintrag } from "./Service"
|
||||
import { zugangLesenSynchron, zugangVergessen } from "./zugang"
|
||||
|
||||
/* Wann das Gerät einen Wert zuletzt bestätigt hat — oder dass es das nie
|
||||
getan hat. Steht dort, wo vorher nichts stand, wenn kein Urteil möglich
|
||||
ist: ein stiller Gleichstand mit einem veralteten Wert ist schlimmer als
|
||||
gar kein Vergleich (04.09.2026). Wortgleich mit dongleStand() im Panel. */
|
||||
function dongleStand(w: { gemeldet_am?: string | null }): string {
|
||||
if (!w.gemeldet_am) return "nie bestätigt"
|
||||
const d = new Date(w.gemeldet_am)
|
||||
if (Number.isNaN(d.getTime())) return "nie bestätigt"
|
||||
const tage = Math.floor((Date.now() - d.getTime()) / 86400000)
|
||||
const wann = datum(d)
|
||||
return tage >= 1 ? `bestätigt ${wann} (${tage} T. alt)` : `bestätigt ${wann}`
|
||||
}
|
||||
|
||||
export function Einstellungen({
|
||||
tabBeschriftung,
|
||||
setzeTabBeschriftung,
|
||||
@@ -964,13 +977,13 @@ export function Einstellungen({
|
||||
key={w.schluessel}
|
||||
label={`${w.titel} ${w.nummer}`}
|
||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||
warnung={Boolean(w.abweichung)}
|
||||
warnung={w.abweichung === true}
|
||||
zusatz={
|
||||
w.offen != null
|
||||
? `→ ${w.offen}, unterwegs`
|
||||
: w.abweichung
|
||||
: w.abweichung === true
|
||||
? `(wir: ${w.unser})`
|
||||
: undefined
|
||||
: dongleStand(w)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -269,6 +269,23 @@ def _sicht(eintraege: list[dict], feld: str) -> dict[str, object]:
|
||||
return flach
|
||||
|
||||
|
||||
def _stempel(eintraege: list[dict]) -> dict[str, float]:
|
||||
"""Wann das GERAET jede Einstellung zuletzt bestaetigt hat.
|
||||
|
||||
flespi legt zu jedem Eintrag ein `updated` (Unix-Sekunde). Das ist der
|
||||
Zeitpunkt, zu dem der Wert vom Geraet kam - nicht der unseres Abrufs.
|
||||
Genau daran fehlte es am 04.09.2026: `trip_scenario` stand auf dem Stand
|
||||
vom 01.09. 13:15, waehrend die aufgespielte Konfiguration das Szenario
|
||||
laengst abgeschaltet hatte. Der Waechter verglich 900 gegen 900 und meldete
|
||||
Uebereinstimmung - gegen einen dreieinhalb Tage alten Wert.
|
||||
"""
|
||||
return {
|
||||
e["name"]: e["updated"]
|
||||
for e in eintraege
|
||||
if e.get("name") and isinstance(e.get("updated"), (int, float))
|
||||
}
|
||||
|
||||
|
||||
def _roh(eintraege: list[dict], name: str) -> dict | None:
|
||||
for eintrag in eintraege:
|
||||
if eintrag.get("name") == name:
|
||||
@@ -281,8 +298,20 @@ def vergleichen(
|
||||
aktuell: dict[str, object],
|
||||
offen: dict[str, object],
|
||||
wunsch: int | None,
|
||||
stempel: dict[str, float] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben."""
|
||||
"""Die interessanten Einstellungen mit unserem eigenen Wert daneben.
|
||||
|
||||
`abweichung` ist DREIWERTIG: True (weicht ab), False (stimmt ueberein) und
|
||||
**None (kein Urteil)**. None steht dort, wo ein Vergleich nichts aussagen
|
||||
wuerde - weil das Geraet den Wert nie bestaetigt hat, oder weil gerade eine
|
||||
Aenderung zu ihm unterwegs ist und der Wert sich ohnehin gleich aendert.
|
||||
|
||||
Warum das noetig wurde: bis zum 04.09.2026 war `abweichung` ein blosses
|
||||
Bool, und `False` hiess "stimmt ueberein" - auch dann, wenn der verglichene
|
||||
Wert Tage alt war. Ein stiller Gleichstand mit einem veralteten Wert ist
|
||||
schlimmer als gar kein Vergleich: er behauptet Sicherheit, wo keine ist.
|
||||
"""
|
||||
from . import fahrterkennung
|
||||
|
||||
zeilen = []
|
||||
@@ -298,11 +327,17 @@ def vergleichen(
|
||||
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)
|
||||
)
|
||||
# Der Name der Einstellung ist der Teil vor dem ersten Punkt -
|
||||
# "sleep_mode.mode.timeout" gehoert zu "sleep_mode".
|
||||
name = schluessel.split(".")[0]
|
||||
gemeldet = (stempel or {}).get(name)
|
||||
wartet = schluessel in offen and offen.get(schluessel) != geraet
|
||||
vergleichbar = isinstance(geraet, (int, float)) and isinstance(unser, (int, float))
|
||||
if not vergleichbar or gemeldet is None or wartet:
|
||||
# Kein Gegenstueck bei uns, nie bestaetigt, oder gerade unterwegs.
|
||||
abweichung: bool | None = None
|
||||
else:
|
||||
abweichung = int(geraet) != int(unser)
|
||||
zeilen.append(
|
||||
{
|
||||
"schluessel": schluessel,
|
||||
@@ -322,6 +357,13 @@ def vergleichen(
|
||||
),
|
||||
"unser": unser,
|
||||
"abweichung": abweichung,
|
||||
# Wann das Geraet diesen Wert zuletzt bestaetigt hat. None
|
||||
# heisst: nie - dann steht daneben kein Urteil, sondern nichts.
|
||||
"gemeldet_am": (
|
||||
datetime.datetime.fromtimestamp(gemeldet, datetime.UTC).isoformat()
|
||||
if gemeldet is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return zeilen
|
||||
@@ -357,26 +399,97 @@ async def _alle_einstellungen(
|
||||
return eintraege
|
||||
|
||||
|
||||
def _stand(geraet: int, eintraege: list[dict], wunsch: int | None) -> dict:
|
||||
def _stand(
|
||||
geraet: int,
|
||||
eintraege: list[dict],
|
||||
wunsch: int | None,
|
||||
geleert: list[str] | None = None,
|
||||
) -> dict:
|
||||
aktuell = _sicht(eintraege, "current")
|
||||
offen = _sicht(eintraege, "pending")
|
||||
werte = vergleichen(aktuell, offen, wunsch)
|
||||
werte = vergleichen(aktuell, offen, wunsch, _stempel(eintraege))
|
||||
return {
|
||||
"geraet": geraet,
|
||||
"gelesen_am": jetzt_iso(),
|
||||
"anzahl": len(eintraege),
|
||||
"werte": werte,
|
||||
"abweichungen": sum(1 for z in werte if z["abweichung"]),
|
||||
# Welche Zwischenspeicher dieser Abruf geleert hat - leer, wenn nur
|
||||
# gelesen wurde.
|
||||
"geleert": geleert or [],
|
||||
"abweichungen": sum(1 for z in werte if z["abweichung"] is True),
|
||||
# Zeilen ohne Urteil: nie bestaetigt oder gerade unterwegs.
|
||||
"unbekannt": sum(1 for z in werte if z["abweichung"] is None),
|
||||
"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."""
|
||||
# Die Namen der Einstellungen, die wir ueberwachen - der Teil vor dem ersten
|
||||
# Punkt in INTERESSANT ("sleep_mode.mode.timeout" -> "sleep_mode").
|
||||
UEBERWACHT = sorted({schluessel.split(".")[0] for schluessel in INTERESSANT})
|
||||
|
||||
|
||||
async def _cache_leeren(
|
||||
hass: HomeAssistant, token: str, geraet: int, eintraege: list[dict]
|
||||
) -> list[str]:
|
||||
"""Loescht flespis Zwischenspeicher fuer die ueberwachten Einstellungen.
|
||||
|
||||
WAS DAS TUT UND WAS NICHT
|
||||
-------------------------
|
||||
`DELETE /gw/devices/{id}/settings/{name}` wirft den GESPEICHERTEN Wert weg,
|
||||
nicht den im Geraet. flespi fragt ihn beim naechsten Verbinden neu ab. Es
|
||||
ist die API-Entsprechung des Knopfes "clear cache and synchronize" im
|
||||
flespi-Panel.
|
||||
|
||||
Ohne das liest jeder Abruf nur den Zwischenspeicher zurueck. Am 04.09.2026
|
||||
stand dort fuer `trip_scenario` der Stand vom 01.09. 13:15 - dreieinhalb
|
||||
Tage alt, und das Szenario war laengst abgeschaltet.
|
||||
|
||||
AUSSTEHENDE WERTE BLEIBEN UNANGETASTET
|
||||
--------------------------------------
|
||||
Dasselbe DELETE verwirft auch ein `pending`. Stuende gerade eine Aenderung
|
||||
in der Warteschlange zum Geraet - wie `1003`/`1004` am 04.09. acht Stunden
|
||||
lang -, waere sie damit weg, ohne dass es jemand merkt. Solche
|
||||
Einstellungen werden uebersprungen; ihr Wert aendert sich ohnehin gleich.
|
||||
"""
|
||||
geleert: list[str] = []
|
||||
for eintrag in eintraege:
|
||||
name = eintrag.get("name")
|
||||
if name not in UEBERWACHT:
|
||||
continue
|
||||
if eintrag.get("pending") is not None:
|
||||
_LOGGER.info(
|
||||
"%s hat einen ausstehenden Wert - Zwischenspeicher nicht "
|
||||
"geleert, sonst waere die Aenderung verworfen", name,
|
||||
)
|
||||
continue
|
||||
try:
|
||||
await _anfrage(hass, token, "DELETE", f"/gw/devices/{geraet}/settings/{name}")
|
||||
except FlespiFehler as fehler:
|
||||
# Ein misslungenes Leeren ist kein Grund, das Lesen abzubrechen -
|
||||
# dann steht eben der alte Wert da, so wie bisher auch.
|
||||
_LOGGER.warning("Zwischenspeicher von %s nicht geleert: %s", name, fehler)
|
||||
continue
|
||||
geleert.append(name)
|
||||
return geleert
|
||||
|
||||
|
||||
async def lesen(k: Koordinator, frisch: bool = False) -> dict:
|
||||
"""Holt die Konfiguration und gibt den fertigen Anzeigestand zurück.
|
||||
|
||||
Mit `frisch` wird vorher der Zwischenspeicher geleert (siehe
|
||||
`_cache_leeren`). Die Werte, die danach zurueckkommen, koennen dadurch
|
||||
fehlen - dann hat das Geraet sie noch nicht bestaetigt, und genau das
|
||||
soll man sehen.
|
||||
"""
|
||||
token, geraet = await _geraet_und_token(k)
|
||||
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||
return _stand(geraet, eintraege, wunsch_timeout(k))
|
||||
geleert: list[str] = []
|
||||
if frisch:
|
||||
geleert = await _cache_leeren(k.hass, token, geraet, eintraege)
|
||||
if geleert:
|
||||
eintraege = await _alle_einstellungen(k.hass, token, geraet)
|
||||
return _stand(geraet, eintraege, wunsch_timeout(k), geleert)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- Schreiben
|
||||
@@ -545,7 +658,9 @@ async def jetzt_lesen(k: Koordinator) -> None:
|
||||
Antwort, auch auf ein "ging nicht".
|
||||
"""
|
||||
async with k.flespi_sperre:
|
||||
stand = await lesen(k)
|
||||
# Auf Knopfdruck ein ECHTES Lesen: erst den Zwischenspeicher leeren,
|
||||
# dann holen. Sonst kaeme nur zurueck, was flespi ohnehin schon hatte.
|
||||
stand = await lesen(k, frisch=True)
|
||||
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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.25","sha256":"7a53dd0854bfe487c6efc1fc91581e616dc3464bd31baa3356743883f9477331","bytes":324151,"gebaut":"2026-09-04T23:01:16Z"}
|
||||
{"version":"2026.9.4.26","sha256":"0e65b78a949052abdcb6765a9a08e1526e67bec911657073d35f7158f51ba0e4","bytes":324250,"gebaut":"2026-09-04T23:10:56Z"}
|
||||
Binary file not shown.
@@ -3500,6 +3500,19 @@ function csvDatei(name, kopf, zeilen) {
|
||||
a.download = name; document.body.appendChild(a); a.click(); a.remove();
|
||||
}
|
||||
|
||||
/* Wann das Geraet einen Wert zuletzt bestaetigt hat - oder dass es das nie
|
||||
getan hat. Steht dort, wo frueher nichts stand, wenn kein Urteil
|
||||
moeglich ist: ein stiller Gleichstand mit einem veralteten Wert ist
|
||||
schlimmer als gar kein Vergleich (04.09.2026). */
|
||||
function dongleStand(w) {
|
||||
if (!w.gemeldet_am) return "nie bestätigt";
|
||||
const d = new Date(w.gemeldet_am);
|
||||
if (!gueltigesDatum(d)) return "nie bestätigt";
|
||||
const tage = Math.floor((Date.now() - d.getTime()) / 86400000);
|
||||
const wann = dedat(d);
|
||||
return tage >= 1 ? `bestätigt ${wann} (${tage} T. alt)` : `bestätigt ${wann}`;
|
||||
}
|
||||
|
||||
function vEinst() {
|
||||
return `
|
||||
<div class="tile"><span class="label">Fahrzeug einrichten</span>
|
||||
@@ -3759,12 +3772,13 @@ function vEinst() {
|
||||
${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)"` : ""}>${
|
||||
<dd${w.abweichung === true ? ` 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>` : ""
|
||||
: w.abweichung === true ? ` <small>(wir: ${esc(String(w.unser))})</small>`
|
||||
: ` <small style="color:var(--fg3)">${dongleStand(w)}</small>`
|
||||
}</dd>
|
||||
</div>`).join("")}
|
||||
</dl>` : ""}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.25",
|
||||
"version": "2026.9.4.26",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
Reference in New Issue
Block a user