f4222b715f
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>
147 lines
6.6 KiB
Python
147 lines
6.6 KiB
Python
"""Langfristige Aufzeichnung der 12V-Batteriespannung (Mein Audi -> Zustand
|
|
-> Batteriespannung antippen).
|
|
|
|
Eine über die Jahre absinkende Spannung deutet auf eine alternde Batterie hin
|
|
- dafür braucht es einen Verlauf, nicht nur den aktuellen Wert.
|
|
|
|
Ein Eintrag pro Tag ({datum, min, min_ts, max, max_ts}) reicht dafür und hält
|
|
die Datei auch über viele Jahre klein, selbst wenn der Sensor mehrfach pro
|
|
Stunde meldet (aktive Fahrt: Motor an/aus, Ladezustand des Alternators
|
|
wechselt laufend). Die Prüfung läuft deshalb alle 5 Minuten und trägt jede
|
|
Messung in den Tageseintrag ein; die Ablage erweitert dabei nur min/max samt
|
|
dem genauen Zeitstempel der jeweiligen Extremmessung - robust gegen Neustarts
|
|
und schwankende Erreichbarkeit.
|
|
|
|
Der Tagesminimalwert ist die für die Entladung aussagekräftige Größe: er fällt
|
|
typischerweise auf den Moment kurz nach dem Start, bevor der Alternator die
|
|
Ladespannung hochfährt - also am nächsten an die "echte" Ruhespannung heran,
|
|
ohne 2+ Stunden Standzeit abwarten zu müssen. Deshalb verwendet auch die
|
|
Ladezustands-/Gesundheitsauswertung im Frontend (AGM_RUHE_MAX_V) den
|
|
Tagesminimalwert, nicht den Maximalwert.
|
|
|
|
Es wird bewusst NICHT gefiltert, welche Messwerte gespeichert werden - min UND
|
|
max jedes Tages werden roh aufgezeichnet, solange die Messung überhaupt
|
|
plausibel ist (siehe SPANNUNG_MIN_V unten). Die AGM-Ruhespannungs-Grenze wird
|
|
erst beim Anzeigen angewendet: Tage, an denen selbst der Minimalwert über der
|
|
Grenze liegt (Fahrzeug nie im Ruhezustand beobachtet, z. B. bei einer sehr
|
|
langen Fahrt), tauchen dort nicht in der Statistik auf.
|
|
|
|
Eine Ausnahme von "roh aufzeichnen" gibt es: Werte unter SPANNUNG_MIN_V gelten
|
|
als unplausibel für eine echte Messung an dieser 12V-Bleibatterie und deuten
|
|
eher auf einen Sensor-/Verbindungsfehler hin - sie werden gar nicht erst
|
|
aufgezeichnet, damit ein einzelner Ausreißer nicht als Tagesminimum in der
|
|
Liste und im Diagramm landet.
|
|
|
|
Ohne zugeordneten BATTERIE_SENSOR passiert hier bewusst nichts, statt
|
|
Platzhalter-Nullwerte aufzuzeichnen.
|
|
|
|
Optional wird bei jeder neuen Tagesminimum-Messung auch die aktuelle
|
|
Außentemperatur (AUSSENTEMP_SENSOR, CAN ambient air temperature) mit
|
|
aufgezeichnet - für die Messwertliste (Datum, Uhrzeit, Außentemperatur), nicht
|
|
für die Ladezustandsauswertung selbst. Ohne zugeordneten Temperatursensor
|
|
bleibt das Feld None, genau wie beim Batteriesensor.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .veroeffentlichung import zustand_oder_none
|
|
from .verlauf import NEBENWERT_MAX_ABSTAND_S
|
|
|
|
if TYPE_CHECKING:
|
|
from .koordinator import Koordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# Plausible Untergrenze für eine 12V-Bleibatterie (vom Nutzer festgelegt) -
|
|
# darunter ist es ein Sensor-/Verbindungsfehler, keine echte Messung (siehe
|
|
# 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
|
|
if not sensor:
|
|
return
|
|
wert = zustand_oder_none(k.hass, sensor)
|
|
if wert is None:
|
|
return
|
|
try:
|
|
spannung = float(wert)
|
|
except (TypeError, ValueError):
|
|
return
|
|
if spannung < SPANNUNG_MIN_V:
|
|
_LOGGER.warning(
|
|
"Batteriespannung %s V unter der Plausibilitätsgrenze (%s V) - nicht erfasst",
|
|
spannung, SPANNUNG_MIN_V,
|
|
)
|
|
return
|
|
|
|
# Nicht nur "gibt es einen Temperaturwert", sondern "ist er auch aktuell":
|
|
# zustand_oder_none() liefert unabhängig vom Alter den zuletzt bekannten
|
|
# Zustand - meldet der Temperatursensor seltener als die Spannung (im
|
|
# Testbestand teils erst nach 30+ Stunden erneut), würde sonst ein
|
|
# beliebig alter Temperaturwert einer frischen Spannungsmessung
|
|
# zugeschlagen. Verglichen wird gegen den Zeitpunkt der Spannungsmessung
|
|
# selbst (last_updated des Spannungssensors), nicht gegen "jetzt" - beide
|
|
# Sensoren werden aus dem aktuellen hass.states-Stand gelesen, können aber
|
|
# unterschiedlich alt sein. Grenze und Begründung siehe
|
|
# NEBENWERT_MAX_ABSTAND_S (verlauf.py) - vom Nutzer festgelegt.
|
|
aussentemp = None
|
|
temp_sensor = k.zuordnung.werte.AUSSENTEMP_SENSOR
|
|
temp_wert = zustand_oder_none(k.hass, temp_sensor)
|
|
if temp_wert is not None:
|
|
spannung_zustand = k.hass.states.get(sensor)
|
|
temp_zustand = k.hass.states.get(temp_sensor)
|
|
abstand_s = abs((temp_zustand.last_updated - spannung_zustand.last_updated).total_seconds())
|
|
if abstand_s <= NEBENWERT_MAX_ABSTAND_S:
|
|
try:
|
|
aussentemp = float(temp_wert)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
else:
|
|
_LOGGER.debug(
|
|
"Außentemperatur %s s von der Spannungsmessung entfernt - nicht mit erfasst",
|
|
round(abstand_s),
|
|
)
|
|
|
|
jetzt = datetime.datetime.now(datetime.UTC)
|
|
heute = datetime.date.today().isoformat()
|
|
await k.ablage.batterieverlauf_tageswert_aktualisieren(
|
|
heute, jetzt.isoformat(), spannung, aussentemp
|
|
)
|
|
await k.batterieverlauf_veroeffentlichen()
|
|
_LOGGER.debug("Batteriespannung erfasst (%s: %s V)", heute, spannung)
|
|
|
|
|
|
async def eintrag_loeschen(k: Koordinator, datum: str | None) -> None:
|
|
"""Löscht einen Tageseintrag aus der Messwertliste (Wischen zum Löschen).
|
|
|
|
Entfernt nur diesen einen Tag - min/max anderer Tage bleiben unberührt,
|
|
das Diagramm zeichnet beim nächsten Aufruf einfach ohne diesen Punkt."""
|
|
if not datum:
|
|
_LOGGER.warning("batterieverlauf_loeschen ohne datum aufgerufen")
|
|
return
|
|
if await k.ablage.batterieverlauf_eintrag_loeschen(datum):
|
|
await k.batterieverlauf_veroeffentlichen()
|
|
_LOGGER.info("Batteriespannungs-Tageseintrag %s gelöscht", datum)
|
|
else:
|
|
_LOGGER.warning("Batteriespannungs-Tageseintrag %s nicht gefunden", datum)
|