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 <[email protected]>
This commit is contained in:
@@ -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`
|
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
|
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
|
`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
|
stale browser tab's cached pre-restart script, not a new regression). Committed and pushed the same day
|
||||||
this file's own binding lesson from earlier the same day (section W), that should happen before any
|
(`90fff7b`), per this file's own binding lesson from section W.
|
||||||
self-update/install run is suggested or allowed; ask the owner before running one.
|
|
||||||
|
## 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).
|
# Moduldocstring).
|
||||||
SPANNUNG_MIN_V = 10.0
|
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:
|
async def pruefen(k: Koordinator) -> None:
|
||||||
sensor = k.zuordnung.werte.BATTERIE_SENSOR
|
sensor = k.zuordnung.werte.BATTERIE_SENSOR
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ from .const import (
|
|||||||
E_ZUORDNUNG,
|
E_ZUORDNUNG,
|
||||||
SIGNAL_AKTUALISIERT,
|
SIGNAL_AKTUALISIERT,
|
||||||
)
|
)
|
||||||
from .veroeffentlichung import fahrzeugstatus
|
from .veroeffentlichung import fahrzeugstatus, spannung_cache_vorladen
|
||||||
from .zuordnung import Zuordnung
|
from .zuordnung import Zuordnung
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -126,6 +126,12 @@ class Koordinator:
|
|||||||
)
|
)
|
||||||
await self.zuordnung.anwenden()
|
await self.zuordnung.anwenden()
|
||||||
await self._laufzeit_laden()
|
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()
|
await self.alles_veroeffentlichen()
|
||||||
# Der Reifenzähler schreibt beim Start einmal fort: Kilometer, die
|
# Der Reifenzähler schreibt beim Start einmal fort: Kilometer, die
|
||||||
# während eines HA-Ausfalls gefahren wurden, gehören noch dem damals
|
# während eines HA-Ausfalls gefahren wurden, gehören noch dem damals
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "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",
|
"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"],
|
||||||
|
|||||||
@@ -27,15 +27,58 @@ from .einstellungen import POSITIONEN, Sensorzuordnung
|
|||||||
|
|
||||||
# Letzte plausible Batteriespannung, für die "Zustand"-Kachel, wenn die
|
# Letzte plausible Batteriespannung, für die "Zustand"-Kachel, wenn die
|
||||||
# aktuelle Live-Messung gerade unplausibel ist (Sensor kurzzeitig offline,
|
# aktuelle Live-Messung gerade unplausibel ist (Sensor kurzzeitig offline,
|
||||||
# Dongle getrennt o.ä.) - der Nutzer will dort die zuletzt gemessene Spannung
|
# Dongle getrennt o.ä. - erwartungsgemäß bei ausgeschaltetem Fahrzeug immer)
|
||||||
# sehen statt "unbekannt". RAM-only wie andere Laufzeitzustände dieses
|
# - der Nutzer will dort die zuletzt gemessene Spannung sehen statt
|
||||||
# Projekts (siehe die bekannten Lücken zu Fahrtstart/Tankstand): kein Store,
|
# "unbekannt", ohne Ablaufdatum. Im RAM statt in einem Store wie andere
|
||||||
# bewusst - ein Neustart zeigt kurz wieder "unbekannt", bis die nächste echte
|
# Laufzeitzustände dieses Projekts (siehe die bekannten Lücken zu
|
||||||
# Messung eintrifft, statt einen möglicherweise sehr alten Wert aus einer
|
# Fahrtstart/Tankstand) - aber koordinator.starten() lädt diesen Wert bei
|
||||||
# Datei wiederzubeleben.
|
# 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}
|
_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:
|
def zustand_oder_none(hass: HomeAssistant, entity_id: str | None) -> str | None:
|
||||||
"""Sicherer Zustandszugriff.
|
"""Sicherer Zustandszugriff.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user