Eine Neuschreibung ist kein Wechsel - Wartezeit startet nicht mehr neu
Home Assistant schreibt denselben Zustandswert auch neu, wenn sich nur ein Attribut geruehrt hat. Am 04.09.2026 gezaehlt: von 22 Zustandszeilen der Zuendung waren nur 6 echte Wechsel, die uebrigen 16 Neuschreibungen - und alle 6 Wechsel lagen ausserhalb der Fahrten. Das Zuendungssignal ist unterwegs stabil. Der Beobachter unterschied das nicht. Fiel eine Neuschreibung von "aus" in die laufende Wartezeit, brach Zweig 2 die wartende Bestaetigung ab und begann die 15 Minuten von vorn. Am 04.09. ist es nicht passiert - die Neuschreibungen lagen hinter dem Fenster -, aber das Loch war da. _signalwechsel() bekommt jetzt mit, ob es ein echter Wechsel war. Eine Neuschreibung, waehrend fuer diese Fahrt schon eine Bestaetigung wartet, laesst sie stehen. Bewusst NICHT als Filter ganz oben: _spaetes_aus() verlaesst sich darauf, ein Zuendungs-Aus auch als off -> off zu sehen - daran haengt das Nachtragen vorlaeufiger Enden. Die Pruefung sitzt nur in Zweig 2, und ein eigener Test belegt, dass der andere Weg weiter offen ist. 21 Tests (von 18), Gegenprobe mit ausgeschalteter Pruefung scheitert wie erwartet an einem. py_compile sauber, audi_ha_test auf 2026.9.4.25 ohne Traceback. AGENTS.md haelt ausserdem fest: die Beschreibung des Beenden-Knopfes war zu weit gefasst (die Korrektur zieht nur nach hinten, nie nach vorn), und der flespi-Cache laesst sich ueber DELETE auf die Einstellung zum Neulesen zwingen - mit dem Haken, dass dabei ausstehende Werte verloren gingen.
This commit is contained in:
@@ -11230,3 +11230,89 @@ Staenden - so sollen sie sein.
|
||||
zweiten Fahrt hielt die Sitzung 18 Minuten durch und schloss normal, der
|
||||
Rueckstand lag durchgehend bei 1-3 Sekunden. **Das wirkt** - die
|
||||
Zustellluecke selbst ist damit aber noch nicht als behoben bewiesen.
|
||||
|
||||
### Nachtrag: eine Neuschreibung ist kein Wechsel (2026.9.4.25)
|
||||
|
||||
Beim Nachgehen der Frage des Eigentuemers - *"warum kommt bei laufendem Motor
|
||||
ueberhaupt ein Ignition-Signal?"* - kam heraus: **es kommt keines.**
|
||||
|
||||
Ueber die Zustandszeilen der Zuendung am 04.09.2026 gezaehlt, getrennt nach
|
||||
echtem Wertwechsel und blosser Neuschreibung:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Zeilen gesamt | 22 |
|
||||
| **echte Wechsel** | **6** |
|
||||
| Neuschreibungen desselben Werts | 16 |
|
||||
|
||||
Und die sechs Wechsel liegen alle ausserhalb der Fahrten: drei beim Anstecken
|
||||
(18:10:27 bis :30, drei Sekunden Einschwingen), zwei beim gepufferten Stapel
|
||||
(23:36:50) und einer am Ziel (23:46:04). **Waehrend beider Fahrten kein
|
||||
einziger.** Das Signal ist unterwegs stabil; das Flackern gehoert zum
|
||||
Anstecken und entfaellt, sobald der Dongle im Fahrzeug bleibt.
|
||||
|
||||
**Der Beobachter unterscheidet das aber nicht.** `_fahrtsignal_geaendert()`
|
||||
reicht `neu` und `alt` ungefiltert weiter, jede Neuschreibung laeuft durch die
|
||||
volle Fahrterkennung. Faellt eine Neuschreibung von "aus" in die laufende
|
||||
Wartezeit, brach Zweig 2 die wartende Bestaetigung ab und begann die 15
|
||||
Minuten von vorn - bei genuegend vielen nie ein Ende.
|
||||
|
||||
**Am 04.09. ist es nicht passiert** (die Neuschreibungen lagen 19,7 Minuten
|
||||
nach dem echten "aus", also hinter dem Fenster). Das Loch war trotzdem da.
|
||||
|
||||
`_signalwechsel()` bekommt jetzt mit, ob es ein echter Wechsel war. Eine
|
||||
Neuschreibung, waehrend fuer DIESE Fahrt schon eine Bestaetigung wartet, laesst
|
||||
sie stehen.
|
||||
|
||||
**Warum nicht einfach `if neu == alt: return` ganz oben:** `_spaetes_aus()`
|
||||
verlaesst sich ausdruecklich darauf, ein Zuendungs-Aus auch dann zu sehen, wenn
|
||||
es "unter Umstaenden zusammen mit dem naechsten on" ankommt - und entscheidet
|
||||
ueber die Geraetezeit, nicht ueber die Reihenfolge. Ein zweites, spaeter
|
||||
gepuffertes Aus bei bereits stehendem "off" waere ein `off -> off` und wuerde
|
||||
von so einem Filter verschluckt. Genau daran haengt das Nachtragen vorlaeufiger
|
||||
Enden. Die Pruefung sitzt deshalb **nur in Zweig 2**.
|
||||
|
||||
**21 Tests** in `test_zuendungspause.py` (von 18). Gegenprobe mit
|
||||
ausgeschalteter Pruefung: **1 Test scheitert**, mit ihr laufen alle 21. Dazu
|
||||
ein Test, der belegt, dass ein `off -> off` ohne laufende Fahrt weiterhin bei
|
||||
`_spaetes_aus()` ankommt - sonst haette ich mir genau das kaputtgemacht, wovor
|
||||
der Kommentar dort warnt.
|
||||
|
||||
### Zum Knopf "Fahrt beenden" - meine Beschreibung war zu weit
|
||||
|
||||
Ich hatte geschrieben, das vorlaeufige Ende sei dafuer da, "dass ein spaeter
|
||||
nachgereichtes Zuendungs-Aus es korrigiert". Der Eigentuemer hat widersprochen,
|
||||
und er hat recht. `_vorlaeufiges_ende_korrigieren()` zieht **nur nach hinten**
|
||||
(`if echt <= ende_bisher: return False` - *"frueher waere kein Nachtrag,
|
||||
sondern eine Verkuerzung"*).
|
||||
|
||||
Am Beispiel des Eigentuemers durchgerechnet - Tankstelle, Motor aus 11:59,
|
||||
Knopf 12:01, Zuendungs-Aus entsteht 12:02:
|
||||
|
||||
echt = 12:02 - 180 s = 11:59 <= vorlaeufiges Ende 12:00 -> keine Korrektur
|
||||
|
||||
Die bewusste Entscheidung an der Zapfsaeule bleibt also stehen. Korrigiert wird
|
||||
nur, wenn danach **wirklich weitergefahren** wurde und der Rest gepuffert
|
||||
nachkommt. Und in beiden Reihenfolgen - Knopf vor oder nach den 180 s - gibt es
|
||||
keine Wartezeit, und das naechste Zuendungssignal beginnt eine neue Fahrt.
|
||||
|
||||
### Der flespi-Cache laesst sich zwingen
|
||||
|
||||
Der Eigentuemer hat auf den Panel-Knopf "clear cache and synchronize"
|
||||
hingewiesen. Die API-Entsprechung ist belegt (zwei Quellen):
|
||||
|
||||
**`DELETE /gw/devices/{id}/settings/{name}`** - *"delete the cached value and
|
||||
thus force flespi to re-read the actual value from the device"*. Es loescht
|
||||
flespis Zwischenspeicher, **nicht** die Einstellung im Geraet.
|
||||
|
||||
**Haken, der in die Umsetzung gehoert:** dasselbe DELETE verwirft auch einen
|
||||
**ausstehenden** Wert. Stuende gerade eine Aenderung in der Warteschlange - wie
|
||||
`1003`/`1004` am 04.09. acht Stunden lang -, waere sie damit weg, ohne dass es
|
||||
jemand merkt. Ein Cache-Loeschen muss jede Einstellung mit `pending`
|
||||
ueberspringen.
|
||||
|
||||
Noch nicht gebaut. Offen bleiben damit:
|
||||
|
||||
* "Jetzt lesen" als echtes Lesen (Cache loeschen, dann holen).
|
||||
* Der Waechter darf keinen Gleichstand melden, solange unbekannt ist, wann der
|
||||
Wert zuletzt vom Geraet bestaetigt wurde.
|
||||
|
||||
@@ -392,11 +392,19 @@ async def zuendung_geaendert(
|
||||
# bisher und der nächste Auslöser schliesst die Fahrt nach. Ein früheres
|
||||
# Löschen hätte sie in dem Fall verloren.
|
||||
async with k.fahrt_sperre:
|
||||
await _signalwechsel(k, neu == "on", ereigniszeit)
|
||||
# `neu != alt` heisst: das ist wirklich ein Wechsel. Home Assistant
|
||||
# schreibt denselben Wert auch neu, wenn sich nur ein Attribut geruehrt
|
||||
# hat - am 04.09.2026 gezaehlt: von 22 Zustandszeilen der Zuendung
|
||||
# waren nur 6 echte Wechsel, die uebrigen 16 blosse Neuschreibungen.
|
||||
# Was das unterscheidet, braucht nur Zweig 2 (siehe dort).
|
||||
await _signalwechsel(k, neu == "on", ereigniszeit, neu != alt)
|
||||
|
||||
|
||||
async def _signalwechsel(
|
||||
k: Koordinator, an_jetzt: bool, ereigniszeit: datetime.datetime | None
|
||||
k: Koordinator,
|
||||
an_jetzt: bool,
|
||||
ereigniszeit: datetime.datetime | None,
|
||||
wechsel: bool = True,
|
||||
) -> None:
|
||||
# Ob eine Fahrt läuft, entscheidet der eigene Zwischenstand
|
||||
# (fahrt_start_ts) - nicht der vorherige Rohwert der Entität. Der wäre bei
|
||||
@@ -422,6 +430,33 @@ async def _signalwechsel(
|
||||
return
|
||||
|
||||
if not an_jetzt and fahrt_laeuft:
|
||||
# Eine blosse Neuschreibung von "aus", waehrend schon eine Bestaetigung
|
||||
# fuer DIESE Fahrt wartet, aendert nichts - und darf die Wartezeit
|
||||
# deshalb nicht von vorn beginnen lassen.
|
||||
#
|
||||
# Ohne diese Pruefung schob jede Neuschreibung das Schliessen um
|
||||
# weitere 15 Minuten hinaus. Am 04.09.2026 ist es nicht passiert - die
|
||||
# Neuschreibungen lagen 19,7 Minuten nach dem echten "aus" und damit
|
||||
# hinter dem Fenster -, aber das Loch war da.
|
||||
#
|
||||
# NUR HIER, nicht am Anfang von zuendung_geaendert(): _spaetes_aus()
|
||||
# weiter unten verlaesst sich ausdruecklich darauf, ein Zuendungs-Aus
|
||||
# auch dann zu sehen, wenn es "unter Umstaenden zusammen mit dem
|
||||
# naechsten on" ankommt. Ein zweites, spaeter gepuffertes Aus bei
|
||||
# bereits stehendem "off" waere ein off -> off - und genau daran haengt
|
||||
# das Nachtragen vorlaeufiger Enden.
|
||||
if (
|
||||
not wechsel
|
||||
and k.ende_wartet is not None
|
||||
and k.ende_wartet[0] == k.fahrt_start_ts
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Zuendung erneut als aus gemeldet, ohne Wechsel - die "
|
||||
"Bestaetigung fuer die Fahrt seit %s laeuft weiter",
|
||||
k.fahrt_start_ts.isoformat(),
|
||||
)
|
||||
await k.fahrzeugstatus_veroeffentlichen()
|
||||
return
|
||||
# Ein neues "aus" ersetzt ein aelteres, das noch wartet.
|
||||
k.warte_ende_ab_abbrechen()
|
||||
# Ob hier noch gewartet wird, entscheidet _pausenzeit_s(): liegt ein
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.4.24","sha256":"2d303b02b3d5ba9d2432856a528b32ed30c657a4ff43e1f39744d4f5c9f8c04a","bytes":324162,"gebaut":"2026-09-04T22:39:43Z"}
|
||||
{"version":"2026.9.4.25","sha256":"7a53dd0854bfe487c6efc1fc91581e616dc3464bd31baa3356743883f9477331","bytes":324151,"gebaut":"2026-09-04T23:01:16Z"}
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.4.24",
|
||||
"version": "2026.9.4.25",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
@@ -337,6 +337,74 @@ class GepufferterStapel(Basis):
|
||||
self.assertEqual(k.beendet, [])
|
||||
self.assertEqual(k.fahrt_start_ts, beginn)
|
||||
|
||||
|
||||
class Neuschreibung(Basis):
|
||||
"""Home Assistant schreibt denselben Wert auch neu.
|
||||
|
||||
Am 04.09.2026 gezaehlt: von 22 Zustandszeilen der Zuendung waren nur 6
|
||||
echte Wechsel. Die uebrigen 16 waren Neuschreibungen - der Wert selbst
|
||||
hatte sich nicht geruehrt.
|
||||
|
||||
Der Beobachter unterscheidet das nicht, er reicht neu und alt durch. Faellt
|
||||
so eine Neuschreibung von "aus" in die laufende Wartezeit, begann die
|
||||
Wartezeit von vorn - und bei genuegend vielen nie ein Ende.
|
||||
"""
|
||||
|
||||
async def test_neuschreibung_verlaengert_die_wartezeit_nicht(self):
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
wartet = k.ende_wartet
|
||||
self.assertIsNotNone(wartet, "nach dem Aus wartet eine Bestaetigung")
|
||||
|
||||
# Dieselbe Meldung noch einmal, ohne Wertwechsel.
|
||||
await f.zuendung_geaendert(k, "off", "off", AUS + datetime.timedelta(seconds=30))
|
||||
self.assertIs(
|
||||
k.ende_wartet, wartet, "es wartet weiterhin dieselbe Bestaetigung"
|
||||
)
|
||||
|
||||
await k.ausklingen()
|
||||
self.assertEqual(len(k.beendet), 1, "und sie kommt zum Zug")
|
||||
self.assertEqual(
|
||||
k.beendet[0][1],
|
||||
AUS - datetime.timedelta(seconds=f.ZUENDUNG_NACHLAUF_S),
|
||||
"mit dem Ende aus dem ERSTEN Aus, nicht dem der Neuschreibung",
|
||||
)
|
||||
|
||||
async def test_echtes_zweites_aus_zaehlt_weiterhin(self):
|
||||
"""Die Gegenprobe: ein Wechsel darf die Bestaetigung sehr wohl ersetzen."""
|
||||
k = FakeKoordinator()
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS)
|
||||
erstes = k.ende_wartet
|
||||
await f.zuendung_geaendert(k, "on", "off", AUS + datetime.timedelta(seconds=30))
|
||||
await f.zuendung_geaendert(k, "off", "on", AUS + datetime.timedelta(seconds=60))
|
||||
self.assertIsNotNone(k.ende_wartet)
|
||||
self.assertNotEqual(
|
||||
k.ende_wartet, erstes, "ein echtes zweites Aus setzt ein neues Ende"
|
||||
)
|
||||
|
||||
async def test_neuschreibung_ohne_laufende_fahrt_geht_weiter_durch(self):
|
||||
"""_spaetes_aus() braucht auch ein off -> off.
|
||||
|
||||
Ein zweites, spaeter gepuffertes Aus traegt das Ende einer von Hand
|
||||
beendeten Fahrt nach. Ein Filter ganz oben haette genau das
|
||||
verschluckt - deshalb sitzt die Pruefung nur in Zweig 2.
|
||||
"""
|
||||
gesehen = []
|
||||
|
||||
async def spaetes_aus(k, ereigniszeit, jetzt):
|
||||
gesehen.append(ereigniszeit)
|
||||
|
||||
echt = f._spaetes_aus
|
||||
f._spaetes_aus = spaetes_aus
|
||||
try:
|
||||
k = FakeKoordinator(start_ts=None)
|
||||
await f.zuendung_geaendert(k, "off", "off", AUS)
|
||||
self.assertEqual(
|
||||
len(gesehen), 1, "ohne laufende Fahrt muss das Aus ankommen"
|
||||
)
|
||||
finally:
|
||||
f._spaetes_aus = echt
|
||||
|
||||
class Einstellung(unittest.IsolatedAsyncioTestCase):
|
||||
"""_pausenzeit_s selbst - hier ungepatcht."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user