172db54e7a
- Statistik-Tab-Icon per zentrierter Stauchung an die Fuellflaeche der anderen Tab-Icons angeglichen (Panel + companion-app), auf Nutzerwunsch anschliessend 10% groesser. - Batteriediagramm neu gezeichnet: dynamische viewBox (1 Einheit = 1 Pixel, kein preserveAspectRatio-Verzerren mehr), ein Punkt je Ruhespannungsmessung statt Datumsbeschriftung, Tageshoechstwert nur noch im Tooltip. Generator- spannungen (> AGM_RUHE_MAX_V = 13,0 V, vom Nutzer festgelegt) bleiben aus dem Diagramm aussen vor; der SOC-Kurve-Eckwert "100% / voll" bleibt bei den ursprünglich recherchierten 12,8 V. companion-app auf dieselbe lineare SOC-Interpolation umgestellt (vorher eine abweichende Stufenkurve). - Außentemperatur wird jetzt auch beim Batterieverlauf-Import nachgetragen (vorher nur live) - Kopplung an die naechstgelegene Messung innerhalb NEBENWERT_MAX_ABSTAND_S (verlauf.py), vom Nutzer nach Live-Daten-Analyse auf 300s festgelegt. Merge-Logik in ablage.py ergaenzt: ein erneuter Import kann eine zuvor fehlende Temperatur jetzt tatsaechlich nachtragen, auch wenn sich der Tagesminimalwert selbst nicht aendert. - "Mein Audi"/Zustand zeigt bei unplausibler Live-Messung (z. B. Dongle offline) die zuletzt gemessene Spannung statt "unbekannt". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
133 lines
5.8 KiB
Python
133 lines
5.8 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
|
|
|
|
|
|
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)
|