Batteriespannung-Fallback: zeigt letzte Ruhespannungsmessung ueber Neustarts hinweg

Der RAM-only Fallback fuer die Zustand-Kachel wurde bei jedem Neustart
geleert und zeigte "unbekannt", solange seit dem Neustart keine echte
Live-Messung eintraf (Dongle bei ausgeschaltetem Fahrzeug erwartungsgemaess
immer offline). spannung_cache_vorladen() laedt ihn jetzt beim Start aus
der gespeicherten Historie vor, begrenzt auf den vereinbarten Bereich
10,0-13,0 V (SPANNUNG_MIN_V bis AGM_RUHE_MAX_V) - eine Generatorspannung
(z.B. 15,4 V) soll dort nie als Batteriespannung erscheinen.

Version 2026.8.27.21, im Testcontainer verifiziert (12,149 V statt
"unbekannt", trotz Dongle weiterhin 0 V live).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 00:23:16 +02:00
parent 90fff7b305
commit f4222b715f
5 changed files with 126 additions and 11 deletions
+55 -3
View File
@@ -3853,9 +3853,61 @@ Verified: all five touched backend files (`verlauf.py`, `historienimport.py`, `f
changed), `npm run ota` rerun (companion-app source changed, `Batterie.tsx`/`Statistik.tsx`), `audi_ha_test`
restarted and confirmed clean via log (`Audi Dashboard 2026.8.27.19 eingerichtet`, no traceback; the only
`ReferenceError: bvRuhePunkte` lines in the post-restart log predate the setup-complete line and come from a
stale browser tab's cached pre-restart script, not a new regression). **Not yet committed/pushed** - per
this file's own binding lesson from earlier the same day (section W), that should happen before any
self-update/install run is suggested or allowed; ask the owner before running one.
stale browser tab's cached pre-restart script, not a new regression). Committed and pushed the same day
(`90fff7b`), per this file's own binding lesson from section W.
## Z. Battery-voltage fallback didn't survive a restart - now seeded from disk (2026.8.27.20)
Owner report right after the section Y fixes: "batteriespannung is still unbekannt!" - live investigation
(browser session against `audi_ha_test` at `:18123`, confirmed reachable per section W's note) found this
was NOT a regression from section Y: the FMM003 dongle was genuinely reporting `external_power_voltage: 0`
live (confirmed via `hass.states` in the browser console), and `_letzte_gueltige_spannung`
(`veroeffentlichung.py`, the RAM-only "last known good voltage" fallback added in section W) had been wiped
by the container restarts done while verifying section Y's fixes - it had no valid reading to fall back to
since the last restart, so "unbekannt" was the technically-correct result of the existing design.
Owner's response: **"wait. I want to see the last measured voltage!"**, then, pre-empting a staleness
concern before it was raised: **"no lifedate! The dongle will be always offline when car is off"** - i.e.
the RAM-only, resets-on-restart design (deliberately chosen in section W to avoid "resurrecting a possibly
very old value") no longer matches what's wanted: the dongle being offline is the *expected* steady state
whenever the car is parked, so the fallback needs to survive restarts, and its age must never be a reason to
hide it - an old real reading is still better than "unbekannt".
Fixed by seeding the RAM cache from the already-persisted `batteriespannung.jsonl` history once, at startup,
instead of changing the cache to a Store (would duplicate data already saved elsewhere for a different
purpose - the day-level battery history). New `spannung_cache_vorladen(verlauf)` in `veroeffentlichung.py`,
called once from `Koordinator.starten()` (`koordinator.py`) right before the first `alles_veroeffentlichen()`
so even the very first publish after a restart already carries the seeded value instead of a transient
"unbekannt". `fahrzeugstatus()` itself (the per-tick publish function) is untouched - it already reads
`_letzte_gueltige_spannung["wert"]` as its fallback, this just means that value is no longer empty
immediately after a restart.
**First version was wrong - owner caught it: "you got me wrong". "last measured voltage" pulled from the
raw `min`/`max` of the last day entry regardless of value - which on this container's real data meant
15,4 V, the day's *max*, i.e. generator/charging voltage (`batterie.py`'s own docstring documents that
min/max are stored raw, unfiltered by any resting-voltage border - see section N/T history).** A 15,4 V
"Batteriespannung" reading is misleading - that's not the battery's own state. Owner: **"last measured
voltage - but within the agreed borders! confirm me the border!"** - confirmed and agreed: **10,0 V
(`SPANNUNG_MIN_V`, the existing plausibility floor) to 13,0 V (`AGM_RUHE_MAX_V`, the existing
resting-vs-generator ceiling)** - the same two borders already governing the live reading itself
(`fahrzeugstatus()`) and the frontend's diagram/trend filtering (section X), just not previously shared
into the backend as a named constant. Added `AGM_RUHE_MAX_V = 13.0` to `batterie.py` (backend's own copy,
next to `SPANNUNG_MIN_V` - `veroeffentlichung.py` imports both locally inside the function to avoid the
same circular-import risk `SPANNUNG_MIN_V` already routes around). `spannung_cache_vorladen()` rewritten to
scan the ENTIRE stored history (not just the last day), collect every `min`/`max` reading that falls inside
`[SPANNUNG_MIN_V, AGM_RUHE_MAX_V]`, and seed the cache with the chronologically most recent one - so a day
where the car was never at rest (both min and max above 13,0 V) is correctly skipped in favor of the most
recent day that does have a genuine resting-range reading.
Verified: `py_compile` clean on all touched files, manifest bumped `2026.8.27.20``2026.8.27.21`,
`audi_ha_test` restarted twice and confirmed clean via log both times (`... eingerichtet`, no traceback) with
the dongle still reporting `0 V` live throughout. First (unfiltered) version showed **15,4 V** live - correct
per that version's own logic, wrong per the actual requirement, caught by the owner before being accepted.
Corrected version confirmed live via the published entity's own `daten.batteriespannung` attribute:
**12,149 V** - the most recent stored reading that actually falls within 10,0-13,0 V (2026-08-27's daily
minimum, `12.149` at `09:44:00Z`), correctly skipping that same day's `15.439` maximum and every one of the
three preceding fully-out-of-border days (`2026-08-23`/`24`/`25`, both min and max pinned at `13.988`, always
excluded).
---
@@ -61,6 +61,20 @@ _LOGGER = logging.getLogger(__name__)
# Moduldocstring).
SPANNUNG_MIN_V = 10.0
# Obergrenze für eine echte Ruhespannungsmessung (vom Nutzer festgelegt: "max
# voltage for AGM Battery is 13V, everything above is the generator") -
# darüber ist es Generator-/Ladespannung, keine Batteriespannung mehr.
# Identisch mit AGM_RUHE_MAX_V im Frontend (audi-dashboard-app.js/
# Batterie.tsx), dort aber nur für die Anzeige (welche Tage ins Ladezustands-
# /Trend-Diagramm einfließen - siehe Moduldocstring oben, "erst beim Anzeigen
# angewendet"). Hier zusätzlich fürs Backend gebraucht, als oberer Rand für
# spannung_cache_vorladen() (veroeffentlichung.py): der "zuletzt gemessene
# Wert"-Fallback für die Zustand-Kachel soll ausdrücklich innerhalb von
# [SPANNUNG_MIN_V, AGM_RUHE_MAX_V] bleiben - eine Generatorspannung soll dort
# nie als "Batteriespannung" erscheinen, auch nicht als letzter bekannter
# Wert.
AGM_RUHE_MAX_V = 13.0
async def pruefen(k: Koordinator) -> None:
sensor = k.zuordnung.werte.BATTERIE_SENSOR
@@ -67,7 +67,7 @@ from .const import (
E_ZUORDNUNG,
SIGNAL_AKTUALISIERT,
)
from .veroeffentlichung import fahrzeugstatus
from .veroeffentlichung import fahrzeugstatus, spannung_cache_vorladen
from .zuordnung import Zuordnung
_LOGGER = logging.getLogger(__name__)
@@ -126,6 +126,12 @@ class Koordinator:
)
await self.zuordnung.anwenden()
await self._laufzeit_laden()
# Die "Zustand"-Kachel soll die zuletzt gemessene Batteriespannung
# auch direkt nach einem Neustart zeigen, nicht erst "unbekannt" bis
# zur nächsten Live-Messung (siehe spannung_cache_vorladen()) - vor
# dem ersten alles_veroeffentlichen(), damit schon dessen erste
# Veröffentlichung den vorgeladenen Wert trägt.
spannung_cache_vorladen(await self.ablage.batterieverlauf_lesen())
await self.alles_veroeffentlichen()
# Der Reifenzähler schreibt beim Start einmal fort: Kilometer, die
# während eines HA-Ausfalls gefahren wurden, gehören noch dem damals
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.27.19",
"version": "2026.8.27.21",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
@@ -27,15 +27,58 @@ from .einstellungen import POSITIONEN, Sensorzuordnung
# Letzte plausible Batteriespannung, für die "Zustand"-Kachel, wenn die
# aktuelle Live-Messung gerade unplausibel ist (Sensor kurzzeitig offline,
# Dongle getrennt o.ä.) - der Nutzer will dort die zuletzt gemessene Spannung
# sehen statt "unbekannt". RAM-only wie andere Laufzeitzustände dieses
# Projekts (siehe die bekannten Lücken zu Fahrtstart/Tankstand): kein Store,
# bewusst - ein Neustart zeigt kurz wieder "unbekannt", bis die nächste echte
# Messung eintrifft, statt einen möglicherweise sehr alten Wert aus einer
# Datei wiederzubeleben.
# Dongle getrennt o.ä. - erwartungsgemäß bei ausgeschaltetem Fahrzeug immer)
# - der Nutzer will dort die zuletzt gemessene Spannung sehen statt
# "unbekannt", ohne Ablaufdatum. Im RAM statt in einem Store wie andere
# Laufzeitzustände dieses Projekts (siehe die bekannten Lücken zu
# Fahrtstart/Tankstand) - aber koordinator.starten() lädt diesen Wert bei
# jedem Neustart einmalig aus der gespeicherten Historie vor
# (spannung_cache_vorladen()), bevor die erste Veröffentlichung läuft, damit
# ein Neustart NICHT mehr kurz "unbekannt" zeigt.
_letzte_gueltige_spannung: dict[str, float | None] = {"wert": None}
def spannung_cache_vorladen(verlauf: list[dict]) -> None:
"""Lädt die "Zustand"-Kachel-Fallback-Spannung beim Start aus der
gespeicherten Historie (batteriespannung.jsonl) vor, statt bis zur
nächsten echten Live-Messung nach jedem Neustart erst "unbekannt" zu
zeigen.
Der Dongle ist bei ausgeschaltetem Fahrzeug erwartungsgemäß immer
offline (Nutzerangabe) - "die zuletzt gemessene Spannung" soll deshalb
auch über einen Neustart hinweg sichtbar bleiben, und zwar ohne
Ablaufdatum: hier gibt es bewusst KEINE Prüfung, wie alt der Wert ist -
ein alter, aber echter Messwert ist der Kachel immer noch lieber als
"unbekannt".
Aber nur innerhalb der vereinbarten Grenze [SPANNUNG_MIN_V,
AGM_RUHE_MAX_V] (10,0-13,0 V, batterie.py): min/max werden roh
gespeichert (siehe batterie.py's Moduldocstring) und können deshalb auch
Generatorspannung enthalten (z. B. ein Tagesmaximum von 15,4 V bei
laufendem Alternator) - so ein Wert soll nie als "Batteriespannung"
auf der Zustand-Kachel landen, auch nicht als letzter bekannter Wert.
Durchsucht deshalb die GESAMTE Historie (nicht nur den letzten
Tageseintrag) nach dem zeitlich jüngsten min- oder max-Wert, der
innerhalb der Grenze liegt - ein Tag, an dem das Fahrzeug nie im
Ruhezustand beobachtet wurde (beide Werte über der Grenze), wird dabei
übersprungen, der davor gemessene gültige Wert bleibt der Fallback."""
from .batterie import AGM_RUHE_MAX_V, SPANNUNG_MIN_V
kandidaten: list[tuple[str, float]] = []
for eintrag in verlauf:
for wert_feld, ts_feld in (("min", "min_ts"), ("max", "max_ts")):
wert = eintrag.get(wert_feld)
ts = eintrag.get(ts_feld)
if wert is None or not ts:
continue
if SPANNUNG_MIN_V <= wert <= AGM_RUHE_MAX_V:
kandidaten.append((ts, wert))
if not kandidaten:
return
kandidaten.sort(key=lambda p: p[0])
_letzte_gueltige_spannung["wert"] = kandidaten[-1][1]
def zustand_oder_none(hass: HomeAssistant, entity_id: str | None) -> str | None:
"""Sicherer Zustandszugriff.