Kalender ueber EventKit, Orte nach dem Import, Fotos verkleinert, Signalsperre (2026.9.3.1-.3)

- Kalendertermine gehen nativ ueber EventKit statt ueber das Teilen-Blatt:
  Apples Kalender meldet sich beim System gar nicht als Teilen-Ziel an, ein
  anderes Dateiformat haette also nie geholfen (Abschnitt BS).
- Der Rueckblick stoesst jetzt selbst das Screening an, wenn er Fahrten
  angelegt hat - sonst blieb eine importierte Fahrt ohne Ort liegen, solange
  das Fahrzeug steht (Abschnitt BT).
- Fahrzeugfotos werden vor dem Upload im Browser auf 2000 px/WebP gebracht.
  Gemessen: 3.099.022 -> 325.994 Bytes. Damit ist die Nachrichtengrenze der
  WebSocket-Verbindung kein Thema mehr ("connection lost" auf der realen
  Instanz). Formate, die der Browser weder umwandeln noch anzeigen kann,
  werden mit klarer Meldung abgelehnt statt roh gespeichert.
- Zwei Wechsel des Fahrtsignals in derselben Sekunde kosteten eine ganze
  Fahrt: das "on" ueberholte den noch laufenden Beende-Vorgang und fiel durch
  beide Zweige. Neue asyncio-Sperre plus Regressionstest, der ohne sie
  nachweislich rot ist (Abschnitt BU).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-03 01:59:32 +02:00
co-authored by Claude Opus 5
parent 8c95e50554
commit 6016053a92
18 changed files with 1072 additions and 78 deletions
@@ -254,11 +254,39 @@ async def zuendung_geaendert(
# abbrechen - das ist der Mechanismus hinter der Pausenregel.
k.warte_ende_ab_abbrechen()
an_jetzt = neu == "on"
# Von hier an nur einer zur Zeit. Home Assistant startet für jeden
# Zustandswechsel eine eigene Aufgabe, und dieser Rumpf wartet mehrfach:
# _geraetezeit() bis zu zwei Sekunden auf die Meldezeit, danach liest
# _echtes_ende() den Verlauf, danach schreibt fahrt_beenden() die Fahrt
# und stösst das Screening an.
#
# Ohne die Sperre überholt der nächste Wechsel den laufenden. Genau das ist
# am 02.09.2026 passiert und hat eine ganze Fahrt gekostet: das Gerät hatte
# gepuffert, deshalb kamen "Fahrt zu Ende" (Gerätezeit 21:20:55) und "Fahrt
# begonnen" in DERSELBEN Sekunde bei Home Assistant an, 21:22:12. Das "off"
# ging in den Beende-Zweig und wartete; das "on" traf mitten hinein, sah
# fahrt_start_ts noch gesetzt - also "es läuft eine Fahrt" - und passte
# damit in keinen der beiden Zweige unten. Es fiel wortlos durch. Danach
# räumte der Beende-Vorgang das Feld ab, und das reguläre "off" um 21:58:16
# fand keine laufende Fahrt mehr. 36 Minuten Fahrt, live nie gesehen; nur
# der Rückblick hat sie später gefunden.
#
# Die Sperre statt eines früheren Zurücksetzens von fahrt_start_ts: fällt
# der Beende-Vorgang unterwegs aus, bleibt der Zwischenstand stehen wie
# 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)
async def _signalwechsel(
k: Koordinator, an_jetzt: bool, ereigniszeit: datetime.datetime | None
) -> None:
# Ob eine Fahrt läuft, entscheidet der eigene Zwischenstand
# (fahrt_start_ts) - nicht der vorherige Rohwert der Entität. Der wäre bei
# einer Funklücke "unavailable" statt "on" gewesen, obwohl die Fahrt die
# ganze Zeit lief.
# ganze Zeit lief. Gelesen wird er INNERHALB der Sperre: ein Wechsel, der
# auf seinen Vorgänger gewartet hat, muss dessen Ergebnis sehen.
fahrt_laeuft = k.fahrt_start_ts is not None
jetzt = datetime.datetime.now(datetime.UTC)
@@ -324,6 +352,16 @@ async def nach_neustart_fortsetzen(k: Koordinator) -> None:
if zustand_oder_none(k.hass, sensor) == "on":
return # fährt noch - der Beobachter übernimmt wie sonst auch
# Dieselbe Sperre wie im Beobachter: der erste Wechsel des Fahrtsignals
# kann eintreffen, während dieser Nachlauf noch schreibt.
async with k.fahrt_sperre:
await _nachtraeglich_schliessen(k, sensor)
async def _nachtraeglich_schliessen(k: Koordinator, sensor: str) -> None:
if k.fahrt_start_ts is None:
return # der Beobachter war schneller
ende_ts = await _letztes_lebenszeichen(k, sensor, k.fahrt_start_ts)
_LOGGER.info(
"Fahrt seit %s wurde während eines Ausfalls beendet - wird jetzt mit "