Statistik-Icon-Groesse, Batteriediagramm-Ueberarbeitung, Temperatur-Kopplung mit 300s-Fenster, letzte gueltige Spannung als Fallback
- 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>
This commit is contained in:
@@ -349,7 +349,17 @@ class Ablage:
|
||||
ihn beim ersten Wert des Tages an, erweitert sonst nur min/max samt
|
||||
dem Zeitstempel der jeweils neuen Extremmessung. min_temp_c wird nur
|
||||
mitgeschrieben, wenn diese Messung auch den Minimalwert setzt - sie
|
||||
gilt für genau diesen Zeitpunkt, nicht für den Tag insgesamt."""
|
||||
gilt für genau diesen Zeitpunkt, nicht für den Tag insgesamt.
|
||||
|
||||
Sonderfall, ohne den ein erneuter Import nichts nachliefern könnte:
|
||||
meldet ein späterer Aufruf exakt denselben (bereits gespeicherten)
|
||||
Minimalwert erneut, aber mit einer jetzt verfügbaren Temperatur, wo
|
||||
vorher keine lag - z. B. weil NEBENWERT_MAX_ABSTAND_S seither
|
||||
aufgeweitet wurde und die damals zu weit entfernte Temperaturmessung
|
||||
jetzt innerhalb der Grenze liegt -, wird min_temp_c nachträglich
|
||||
gefüllt. `spannung < eintrag["min"]` allein hätte das nie erlaubt:
|
||||
derselbe Wert ist nie *kleiner* als sich selbst, also griff die
|
||||
Aktualisierung nur beim allerersten Mal."""
|
||||
async with self._sperre:
|
||||
verlauf = await self.batterieverlauf_lesen()
|
||||
for eintrag in verlauf:
|
||||
@@ -359,6 +369,8 @@ class Ablage:
|
||||
eintrag["min"] = spannung
|
||||
eintrag["min_ts"] = ts
|
||||
eintrag["min_temp_c"] = aussentemp
|
||||
elif spannung == eintrag["min"] and eintrag.get("min_temp_c") is None and aussentemp is not None:
|
||||
eintrag["min_temp_c"] = aussentemp
|
||||
if spannung > eintrag["max"]:
|
||||
eintrag["max"] = spannung
|
||||
eintrag["max_ts"] = ts
|
||||
|
||||
@@ -49,6 +49,7 @@ 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
|
||||
@@ -79,13 +80,33 @@ async def pruefen(k: Koordinator) -> None:
|
||||
)
|
||||
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_wert = zustand_oder_none(k.hass, k.zuordnung.werte.AUSSENTEMP_SENSOR)
|
||||
temp_sensor = k.zuordnung.werte.AUSSENTEMP_SENSOR
|
||||
temp_wert = zustand_oder_none(k.hass, temp_sensor)
|
||||
if temp_wert is not None:
|
||||
try:
|
||||
aussentemp = float(temp_wert)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
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()
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.8.27.7","sha256":"4631011c09be09efcdf787ba86ad1bd1fb40006f0d247e8b99b29b0a196f2da3","bytes":235579,"gebaut":"2026-08-27T17:32:23Z"}
|
||||
{"version":"2026.8.27.17","sha256":"9ccedff2ae407751703713cc91b37bb51124f6fb5b98014ef2ae49659e51de2a","bytes":236026,"gebaut":"2026-08-27T21:03:47Z"}
|
||||
Binary file not shown.
@@ -1672,19 +1672,46 @@ function vAudi() {
|
||||
Ziehen mit einem Finger, Antippen wählt den nächstgelegenen Messpunkt.
|
||||
bvZeichnen() aktualisiert nur die SVG/Tooltip-Inhalte (kein render()), damit
|
||||
Zoom/Pan flüssig bleiben. */
|
||||
/* bvPunkte enthält nur Ruhespannungs-Messwerte (<= AGM_RUHE_MAX_V) - sowohl
|
||||
Grundlage für Ladezustand/SOH-Trend als auch für das Diagramm selbst.
|
||||
Generatorspannungen (> AGM_RUHE_MAX_V, z. B. 13,01 V) tauchen dort bewusst
|
||||
nicht auf (Nutzerwunsch: "13,01 shall be generator und not part of the
|
||||
diagram") - anders als in einer Zwischenfassung, die kurzzeitig jede
|
||||
Messung zeichnete; die Messwertliste (vBatterieliste()) zeigt weiterhin
|
||||
ausnahmslos alle Tage samt "Generatorspannung"-Kennzeichnung, sie liest
|
||||
direkt aus BATTERIEVERLAUF, nicht aus bvPunkte. */
|
||||
let bvVollDomain = null, bvDomain = null, bvPunkte = [], bvAusgewaehlt = null, bvYDomain = null;
|
||||
const bvZeiger = new Map(); // pointerId -> {x,y}, für Pinch (2 Finger) vs. Pan (1 Finger)
|
||||
let bvGestStart = null; // {type:"pan"|"pinch", ...Ausgangswerte der Geste}
|
||||
let bvTapKandidat = null, bvBewegtSeit = false;
|
||||
const BV_W = 320, BV_H = 170, BV_ML = 40, BV_MR = 10, BV_MT = 10, BV_MB = 22;
|
||||
/* BV_W ist nur der Startwert: bvZeichnen() setzt die viewBox auf die
|
||||
tatsächlich gerenderte Pixelbreite (bvBreite), damit eine Einheit im
|
||||
Koordinatensystem genau einem Bildschirmpixel entspricht. Vorher stand hier
|
||||
eine feste 320er-viewBox mit preserveAspectRatio="none" - auf der 736px
|
||||
breiten Desktop-Spalte wurde dadurch alles um Faktor 2,3 in die Breite
|
||||
gezogen, sichtbar an den unnatürlich breit gelaufenen Achsenzahlen und an
|
||||
überlappenden Beschriftungen. BV_MB ist klein, seit die Datumsbeschriftung
|
||||
unter dem Diagramm entfällt (Datum steht beim Antippen im Tooltip). */
|
||||
const BV_H = 170, BV_ML = 40, BV_MR = 10, BV_MT = 10, BV_MB = 12;
|
||||
const BV_W = 320;
|
||||
let bvBreite = BV_W;
|
||||
const kurzdat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit" });
|
||||
|
||||
/* Referenzwerte für AGM-Starterbatterien (12V), recherchiert u. a. bei
|
||||
aboutdarwin.com/agm-battery-voltage-charts und brsbattery.com - eine volle
|
||||
AGM-Batterie liegt in Ruhe (min. 2h ohne Last/Ladung) bei 12,8-13,0 V,
|
||||
50 % Ladezustand bei 12,2-12,3 V, unter 11,8 V gilt sie als kritisch
|
||||
entladen. Der Ladealternator liefert im Betrieb üblicherweise 13,5-14,8 V -
|
||||
deutlich über dem Ruhespannungs-Maximum.
|
||||
/* Referenzwerte für AGM-Starterbatterien (12V). Zwei unterschiedliche Zahlen,
|
||||
nicht eine: AGM_RUHE_MAX_V ist die Klassifizierungsgrenze "gilt diese
|
||||
Messung überhaupt noch als Batterie(ruhe)spannung, nicht schon
|
||||
Generatorspannung" (Diagramm/SOH-Trend) - vom Nutzer direkt auf 13,0 V
|
||||
festgelegt ("max voltage for AGM Battery is 13V, everything above is the
|
||||
generator"). BV_SOC_TABELLE (unten) ist davon unabhängig die
|
||||
Ladezustands-Kurve selbst - deren "100 % / voll"-Eckwert bleibt bei den
|
||||
ursprünglich recherchierten 12,8 V (aboutdarwin.com/agm-battery-voltage-
|
||||
charts, brsbattery.com: volle Ruhespannung), vom Nutzer ausdrücklich
|
||||
bestätigt ("12,8v voll was already right"). Eine Messung zwischen 12,8 und
|
||||
13,0 V zählt also als gültige Batteriemessung (Diagrammpunkt, fließt in
|
||||
SOH ein), zeigt aber weiterhin 100 % SOC - dieselbe Klemmung, die die
|
||||
Tabelle für jeden Wert >= ihrem letzten Eckwert ohnehin schon anwendet. Der
|
||||
Ladealternator liefert im Betrieb üblicherweise 13,5-14,8 V - deutlich über
|
||||
beiden Grenzen, die Trennung zur Generatorspannung bleibt also eindeutig.
|
||||
|
||||
Das Backend (batterieverlauf.py) zeichnet pro Tag min UND max roh auf,
|
||||
ohne Filterung - der Sensor meldet künftig mehrfach pro Stunde (aktive
|
||||
@@ -1693,12 +1720,8 @@ const kurzdat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2
|
||||
Moment kurz nach dem Start, bevor der Alternator hochfährt, und ist damit
|
||||
die für den Ladezustand aussagekräftige Größe. Tage, an denen selbst das
|
||||
Minimum über der Grenze liegt (Fahrzeug nie im Ruhezustand beobachtet),
|
||||
tauchen entsprechend nicht in der Ladezustands-Statistik auf. Die Grenze
|
||||
liegt bei 12,8 V (voller Ruhezustand laut obiger Recherche) statt z. B.
|
||||
13,2 V, weil alles darüber bereits vom Generator stammt, nicht von der
|
||||
Batterie selbst - solche Werte würden den Ladezustand künstlich zu gut
|
||||
aussehen lassen. */
|
||||
const AGM_RUHE_MAX_V = 12.8;
|
||||
tauchen entsprechend nicht in der Ladezustands-Statistik auf. */
|
||||
const AGM_RUHE_MAX_V = 13.0;
|
||||
const BV_SOC_TABELLE = [
|
||||
[10.5, 0], [11.8, 10], [11.9, 20], [12.0, 30], [12.1, 40],
|
||||
[12.2, 50], [12.4, 60], [12.5, 70], [12.6, 80], [12.7, 90], [12.8, 100],
|
||||
@@ -1769,12 +1792,17 @@ function bvSohBewertung() {
|
||||
}
|
||||
|
||||
function vBatterieverlauf() {
|
||||
// Der Diagrammpunkt und die Kurve bestehen ausschließlich aus dem
|
||||
// Tagesminimalwert (siehe AGM_RUHE_MAX_V-Kommentar oben) - min_ts ist der
|
||||
// Ein Punkt je Tag mit einer echten Ruhespannungsmessung (<= AGM_RUHE_MAX_V,
|
||||
// z. B. 13,0 V), gezeichnet wird der Tagesminimalwert - min_ts ist der
|
||||
// echte Zeitstempel genau dieser Einzelmessung, nicht ein fiktiver
|
||||
// Tagesmittelpunkt, damit Antippen Datum UND Uhrzeit zeigen kann.
|
||||
bvPunkte = BATTERIEVERLAUF.filter((e) => e.min != null && e.min <= AGM_RUHE_MAX_V)
|
||||
.map((e) => ({ t: new Date(e.min_ts).getTime(), v: e.min, vMin: e.min, vMax: e.max, minTs: e.min_ts, datum: e.datum }))
|
||||
// Tagesmittelpunkt, damit Antippen Datum UND Uhrzeit zeigen kann. Der
|
||||
// Tageshöchstwert bekommt bewusst KEINEN eigenen Punkt, er steht beim
|
||||
// Antippen im Tooltip. Tage, deren Minimum über der Grenze liegt (nie
|
||||
// wirklich in Ruhe beobachtet, sondern nur Generatorspannung), tauchen im
|
||||
// Diagramm nicht auf (Nutzerwunsch) - die Messwertliste zeigt sie weiterhin
|
||||
// alle, mit "Generatorspannung"-Kennzeichnung.
|
||||
bvPunkte = BATTERIEVERLAUF.filter((e) => e.min != null && e.min_ts && e.min <= AGM_RUHE_MAX_V)
|
||||
.map((e) => ({ t: new Date(e.min_ts).getTime(), v: e.min, vMin: e.min, vMax: e.max, minTs: e.min_ts, datum: e.datum, temp: e.min_temp_c }))
|
||||
.sort((a, b) => a.t - b.t);
|
||||
bvAusgewaehlt = null; bvZeiger.clear(); bvGestStart = null;
|
||||
|
||||
@@ -1790,7 +1818,9 @@ function vBatterieverlauf() {
|
||||
bvYDomain = genug ? [10, 15] : null;
|
||||
|
||||
const stufen = bvFarbstufen();
|
||||
const letzter = bvPunkte.length ? bvPunkte[bvPunkte.length - 1] : null;
|
||||
// Ladezustand nur aus der letzten echten Ruhespannung - eine
|
||||
// Generatorspannung ergäbe sonst dauerhaft "100 % · Sehr gut".
|
||||
const letzter = bvRuhePunkte.length ? bvRuhePunkte[bvRuhePunkte.length - 1] : null;
|
||||
const soc = letzter ? bvSocProzent(letzter.v) : null;
|
||||
const soh = bvSohBewertung();
|
||||
const punktFarbe = (status) => (status === "ok" ? "var(--ok)" : status === "warn" ? "var(--warn)" : status === "bad" ? "var(--bad)" : "var(--fg3)");
|
||||
@@ -1806,7 +1836,7 @@ function vBatterieverlauf() {
|
||||
</div>
|
||||
${genug ? `
|
||||
<div id="bvChart" style="margin-top:14px;touch-action:none;position:relative">
|
||||
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;height:${BV_H}px;display:block"></svg>
|
||||
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" style="width:100%;height:${BV_H}px;display:block"></svg>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
|
||||
<span class="label" id="bvRange" style="margin-top:0"></span>
|
||||
@@ -1871,7 +1901,7 @@ function vBatterieliste() {
|
||||
}).join("")}</div>`;
|
||||
}
|
||||
|
||||
function bvSkalaX(t) { return BV_ML + ((t - bvDomain[0]) / (bvDomain[1] - bvDomain[0])) * (BV_W - BV_ML - BV_MR); }
|
||||
function bvSkalaX(t) { return BV_ML + ((t - bvDomain[0]) / (bvDomain[1] - bvDomain[0])) * (bvBreite - BV_ML - BV_MR); }
|
||||
/* Auf [0,1] geklemmter Anteil statt roher Extrapolation - ein Punkt außerhalb
|
||||
von yMin/yMax (z. B. ein Ausreißer, der die Plausibilitätsgrenze auf einer
|
||||
noch nicht aktualisierten Instanz umgangen hat) zeichnet sich sonst weit
|
||||
@@ -1890,8 +1920,10 @@ function bvDomainKlemmen(domain) {
|
||||
if (b > bvVollDomain[1]) { b = bvVollDomain[1]; a = b - breite; }
|
||||
return [a, b];
|
||||
}
|
||||
/* Eine Koordinateneinheit ist seit der dynamischen viewBox genau ein Pixel
|
||||
(siehe bvBreite), die Ränder gelten also unskaliert. */
|
||||
function bvPlotRahmen(rect) {
|
||||
return { linksPx: rect.left + rect.width * (BV_ML / BV_W), breitePx: rect.width * ((BV_W - BV_ML - BV_MR) / BV_W) };
|
||||
return { linksPx: rect.left + BV_ML, breitePx: Math.max(1, rect.width - BV_ML - BV_MR) };
|
||||
}
|
||||
|
||||
function bvZeichnen() {
|
||||
@@ -1902,6 +1934,19 @@ function bvZeichnen() {
|
||||
// nicht mehr "streckt".
|
||||
const [yMin, yMax] = bvYDomain;
|
||||
|
||||
/* Koordinatensystem an die echte Rendergröße koppeln: eine Einheit = ein
|
||||
Pixel. Ohne das zog die feste 320er-viewBox mit preserveAspectRatio="none"
|
||||
auf breiten Spalten alles in die Breite, Achsenzahlen inklusive.
|
||||
Ein ResizeObserver hält das beim Fensterwechsel nach (die viewBox selbst
|
||||
ändert die Elementgröße nicht, deshalb keine Endlosschleife). */
|
||||
const gemessen = svg.getBoundingClientRect().width;
|
||||
if (gemessen > 0) bvBreite = gemessen;
|
||||
svg.setAttribute("viewBox", `0 0 ${bvBreite.toFixed(1)} ${BV_H}`);
|
||||
if (!svg._bvBeobachtet && typeof ResizeObserver !== "undefined") {
|
||||
svg._bvBeobachtet = true;
|
||||
new ResizeObserver(() => bvZeichnen()).observe(svg);
|
||||
}
|
||||
|
||||
// etwas über den sichtbaren Bereich hinaus zeichnen, damit die Linie am
|
||||
// Rand nicht mitten in der Luft abbricht, sondern zum nächsten (Rand-)Punkt
|
||||
// weiterläuft
|
||||
@@ -1911,15 +1956,12 @@ function bvZeichnen() {
|
||||
|
||||
const gitter = [0, 1, 2, 3].map((i) => {
|
||||
const v = yMin + ((yMax - yMin) * i) / 3, y = bvSkalaY(v, yMin, yMax);
|
||||
return `<line x1="${BV_ML}" y1="${y.toFixed(1)}" x2="${BV_W - BV_MR}" y2="${y.toFixed(1)}" stroke="var(--line)" stroke-width="0.5"/>
|
||||
return `<line x1="${BV_ML}" y1="${y.toFixed(1)}" x2="${(bvBreite - BV_MR).toFixed(1)}" y2="${y.toFixed(1)}" stroke="var(--line)" stroke-width="0.5"/>
|
||||
<text x="${BV_ML - 6}" y="${(y + 3).toFixed(1)}" font-size="11" fill="var(--fg3)" text-anchor="end">${de(v, 1)}</text>`;
|
||||
}).join("");
|
||||
const xLabels = [0, 0.5, 1].map((f) => {
|
||||
const x = BV_ML + f * (BV_W - BV_ML - BV_MR);
|
||||
const t = bvDomain[0] + f * spanne;
|
||||
return `<text x="${x.toFixed(1)}" y="${BV_H - 6}" font-size="11" fill="var(--fg3)"
|
||||
text-anchor="${f === 0 ? "start" : f === 1 ? "end" : "middle"}">${kurzdat(new Date(t))}</text>`;
|
||||
}).join("");
|
||||
/* Keine Datumsbeschriftung unter dem Diagramm (Nutzerwunsch): der Zeitraum
|
||||
steht als Text unter der Zeichenfläche (#bvRange), das Datum einer
|
||||
einzelnen Messung erscheint beim Antippen im Tooltip. */
|
||||
const stufen = bvFarbstufen();
|
||||
const kreise = rahmen.map((p) => {
|
||||
const idx = bvPunkte.indexOf(p);
|
||||
@@ -1928,8 +1970,20 @@ function bvZeichnen() {
|
||||
r="${gewaehlt ? 4.5 : 2.4}" fill="${bvFarbe(p.v, stufen)}"
|
||||
${gewaehlt ? 'stroke="var(--fg)" stroke-width="1"' : ""}/>`;
|
||||
}).join("");
|
||||
/* Sanfte Flaechenfuellung unter der Linie (wie iOS Health/Aktie-Charts) -
|
||||
macht aus der duennen Linie auf der jetzt vollen Spaltenbreite ein
|
||||
"fertiges" Diagramm statt eines leeren Bandes. Rein dekorativ, keine
|
||||
neue Information; niedrige Deckkraft, damit das Gitter durchscheint. */
|
||||
const flaeche = rahmen.length >= 2
|
||||
? `<path d="M${bvSkalaX(rahmen[0].t).toFixed(1)},${(BV_H - BV_MB).toFixed(1)} L${linie.split(" ").join(" L")} L${bvSkalaX(rahmen[rahmen.length - 1].t).toFixed(1)},${(BV_H - BV_MB).toFixed(1)} Z"
|
||||
fill="url(#bvGrad)" stroke="none"/>`
|
||||
: "";
|
||||
|
||||
svg.innerHTML = `${gitter}<polyline points="${linie}" fill="none" stroke="var(--fg)" stroke-width="1.5"/>${kreise}${xLabels}`;
|
||||
svg.innerHTML = `<defs><linearGradient id="bvGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="var(--fg)" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="var(--fg)" stop-opacity="0"/>
|
||||
</linearGradient></defs>${gitter}${flaeche}<polyline points="${linie}" fill="none" stroke="var(--fg)"
|
||||
stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>${kreise}`;
|
||||
|
||||
const rangeEl = $("#bvRange");
|
||||
if (rangeEl) rangeEl.textContent = `${kurzdat(new Date(bvDomain[0]))} – ${kurzdat(new Date(bvDomain[1]))}`;
|
||||
@@ -1939,7 +1993,15 @@ function bvZeichnen() {
|
||||
const p = bvPunkte[bvAusgewaehlt];
|
||||
const zeit = new Date(p.minTs);
|
||||
tip.style.display = "block";
|
||||
tip.textContent = `${dedat(zeit)} · ${zeit.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr · Min ${de(p.vMin, 1)} V · Max ${de(p.vMax, 1)} V`;
|
||||
/* Der Höchstwert des Tages hat bewusst keinen eigenen Punkt im Diagramm
|
||||
(Nutzerwunsch) - hier ist die Stelle, an der er sichtbar wird. */
|
||||
const teile = [
|
||||
`${dedat(zeit)} · ${zeit.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr`,
|
||||
`Min ${de(p.vMin, 2)} V`,
|
||||
];
|
||||
if (p.vMax != null) teile.push(`Max ${de(p.vMax, 2)} V`);
|
||||
if (p.temp != null) teile.push(`${de(p.temp, 1)} °C`);
|
||||
tip.textContent = teile.join(" · ");
|
||||
} else {
|
||||
tip.style.display = "none";
|
||||
}
|
||||
@@ -3622,8 +3684,17 @@ const ICONS = {
|
||||
audi: '<path d="M5.46 4.05a80 80 0 0 1 13.08 0l.24.02.14.2A55 55 0 0 1 21.9 9.2l2.1 2.1v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V19l-14-.04v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 19.5v-8.2l2.1-2.1a55 55 0 0 1 2.98-4.93l.14-.2zM2.7 10 1 11.7v.3h4v1H1v6.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.55L20 18v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V13h-4v-1h4v-.3L21.3 10zm17.94-1a45 45 0 0 0-2.43-3.98 78 78 0 0 0-12.44 0L4.51 7c-.3.5-.7 1.16-1.16 2.01h17.3ZM7.3 12h9.42l1.36 1.36-.1.3c-.31 1-.6 1.78-.88 2.31-.13.27-.27.5-.41.67-.13.15-.36.36-.68.36H8a.9.9 0 0 1-.68-.36c-.14-.17-.28-.4-.41-.67a17 17 0 0 1-.89-2.32l-.09-.29zm.42 1-.64.64c.28.84.52 1.47.73 1.89a2.4 2.4 0 0 0 .3.47h7.8l.02-.01c.07-.08.16-.23.28-.46.21-.42.45-1.05.73-1.9L16.3 13H7.71Z"/>',
|
||||
trips: '<circle cx="6" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 7.5c4 1.5 4.5 3 3 5s-1 3.5 4.5 5"/>',
|
||||
/* CI-Symbol polls-s statt der vorherigen handgezeichneten Balken - dieselbe
|
||||
Fuellflaeche-statt-Kontur-Behandlung wie "audi" (siehe ICONS_VOLL). */
|
||||
stat: CI.pollsS,
|
||||
Fuellflaeche-statt-Kontur-Behandlung wie "audi" (siehe ICONS_VOLL). Anders
|
||||
als die handgezeichneten Tab-Icons (die im 24er-Raster deutlich Rand
|
||||
lassen, ~32-47% Fuellflaeche laut Live-Messung per getBBox()) ist das
|
||||
CI-Symbol randlos gezeichnet (~92% Fuellflaeche) und wirkte dadurch neben
|
||||
"Fahrten"/"Tanken" sichtbar zu gross. Zentrierte Stauchung auf das
|
||||
gemessene Grössenverhältnis (Faktor 0.77, um (12,12) - auf Nutzerwunsch
|
||||
10% größer als die erste, auf ~45% Fuellflaeche gerechnete Fassung)
|
||||
statt einer eigenen Icon-Groesse: bleibt so bei jeder Breakpoint-Groesse
|
||||
(.tab svg 26px/22px) automatisch im richtigen Verhältnis, ohne zwei
|
||||
Stellen pflegen zu müssen. */
|
||||
stat: `<g transform="translate(2.76,2.76) scale(0.77)">${CI.pollsS}</g>`,
|
||||
fuel: '<path d="M4.5 20V6.2c0-.9.8-1.7 1.7-1.7h5.6c1 0 1.7.8 1.7 1.7V20"/><path d="M3 20h12.5"/><path d="M4.5 11h10.5"/><path d="M17 20v-8.5a2 2 0 0 0-2-2"/><path d="M17 11.5V7.8l-2-2.3"/>',
|
||||
};
|
||||
const TABS = [
|
||||
|
||||
@@ -52,6 +52,7 @@ from .verlauf import (
|
||||
verbrauch_aus_literstaenden,
|
||||
verlauf_lesen,
|
||||
wert_bei,
|
||||
wert_in_naehe,
|
||||
zahl,
|
||||
)
|
||||
|
||||
@@ -307,10 +308,26 @@ async def _batterie_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
Die Ablage führt bestehende und neue Werte pro Tag zusammen (min bleibt
|
||||
min, max bleibt max), deshalb braucht es hier keine eigene
|
||||
Dubletten-Prüfung: ein zweiter Import desselben Zeitraums verändert die
|
||||
Einträge nicht mehr."""
|
||||
Einträge nicht mehr - das gilt auch für die Außentemperatur (siehe unten),
|
||||
ein erneuter Import über einen bereits importierten Zeitraum ergänzt sie
|
||||
dort nachträglich, ohne etwas doppelt zu schreiben.
|
||||
|
||||
Trägt bei jedem Tagesminimum auch die Außentemperatur nach, sofern
|
||||
AUSSENTEMP_SENSOR zugeordnet ist - Parität zur Live-Erfassung
|
||||
(batterie.py's pruefen()), die das seit ihrer Einführung bereits tut. Bis
|
||||
zu dieser Korrektur fehlte das hier: verlaeufe["aussentemp"] wurde gar
|
||||
nicht erst gelesen, ein Import füllte min_temp_c also nie, selbst wenn die
|
||||
Temperaturhistorie im recorder für denselben Zeitraum längst vorlag.
|
||||
|
||||
wert_in_naehe() statt naechster_wert(): der Temperatursensor dieses
|
||||
Testbestands meldet teils erst nach 30+ Stunden erneut - ohne Zeitgrenze
|
||||
würde ein beliebig alter Temperaturwert der frischen Spannungsmessung
|
||||
zugeschlagen. Siehe NEBENWERT_MAX_ABSTAND_S (verlauf.py) für die vom
|
||||
Nutzer festgelegte Grenze."""
|
||||
verlauf = verlaeufe["batterie"]
|
||||
if not verlauf:
|
||||
return {"tage": 0}
|
||||
aussentemp_verlauf = verlaeufe.get("aussentemp") or []
|
||||
|
||||
tage: dict[str, dict] = {}
|
||||
for ts, wert in verlauf:
|
||||
@@ -336,8 +353,11 @@ async def _batterie_importieren(k: Koordinator, verlaeufe: dict) -> dict:
|
||||
|
||||
for tag in sorted(tage):
|
||||
werte = tage[tag]
|
||||
aussentemp = (
|
||||
wert_in_naehe(werte["min_ts"], aussentemp_verlauf) if aussentemp_verlauf else None
|
||||
)
|
||||
await k.ablage.batterieverlauf_tageswert_aktualisieren(
|
||||
tag, werte["min_ts"].isoformat(), werte["min"]
|
||||
tag, werte["min_ts"].isoformat(), werte["min"], aussentemp
|
||||
)
|
||||
await k.ablage.batterieverlauf_tageswert_aktualisieren(
|
||||
tag, werte["max_ts"].isoformat(), werte["max"]
|
||||
@@ -382,6 +402,7 @@ async def importieren(k: Koordinator, start: object, ende: object) -> None:
|
||||
"tank": await verlauf_lesen(k.hass, werte.TANK_SENSOR, von, bis),
|
||||
"tank_liter": await verlauf_lesen(k.hass, werte.TANK_LITER_SENSOR, von, bis),
|
||||
"batterie": await verlauf_lesen(k.hass, werte.BATTERIE_SENSOR, von, bis),
|
||||
"aussentemp": await verlauf_lesen(k.hass, werte.AUSSENTEMP_SENSOR, von, bis),
|
||||
"lat": await verlauf_lesen(k.hass, werte.STANDORT_LAT_SENSOR, von, bis),
|
||||
"lon": await verlauf_lesen(k.hass, werte.STANDORT_LON_SENSOR, von, bis),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.27.8",
|
||||
"version": "2026.8.27.17",
|
||||
"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"],
|
||||
|
||||
@@ -192,3 +192,43 @@ def naechster_wert(
|
||||
bester_wert = gezahlt
|
||||
beste_diff = diff
|
||||
return bester_wert
|
||||
|
||||
|
||||
# Wie lange eine Nebenmessung (z. B. Außentemperatur) höchstens von der
|
||||
# Hauptmessung entfernt liegen darf, um noch als "zur selben Zeit" zu gelten -
|
||||
# vom Nutzer auf 300s (5 Minuten) festgelegt, nach ursprünglich 60s. Der
|
||||
# Wechsel ist an echten FMM003-Daten begründet, nicht geschätzt: das Gerät
|
||||
# sendet Außentemperatur deutlich seltener als Spannung (an einem
|
||||
# beobachteten Fahrtfenster 851 Spannungs- gegen 187 Temperaturmeldungen in
|
||||
# 48h) - selbst bei laufender Fahrt und ohne jede Verbindungsstörung wurden
|
||||
# reale Lücken bis 593s zwischen zwei Temperaturmeldungen gemessen. 60s hätte
|
||||
# genau diesen völlig normalen Fall verworfen. Physikalisch unproblematisch:
|
||||
# die Außentemperatur ändert sich über wenige Minuten kaum, anders als z. B.
|
||||
# eine GPS-Position oder die Spannung selbst - und die Live-Prüfung
|
||||
# (batterie.py's pruefen()) läuft ohnehin nur alle 5 Minuten, verlangt hier
|
||||
# also keine feinere Genauigkeit, als sie selbst liefert. Ein Verbindungs-
|
||||
# abbruch des Geräts (beobachtet: 45 Minuten, beide Sensoren gleichzeitig
|
||||
# stumm) bleibt mit 300s weiterhin sicher außerhalb der Grenze und liefert
|
||||
# bewusst KEINEN Wert statt eines beliebig alten.
|
||||
NEBENWERT_MAX_ABSTAND_S = 300
|
||||
|
||||
|
||||
def wert_in_naehe(
|
||||
zielzeit: datetime.datetime, verlauf: list[Verlaufspunkt], max_abstand_s: float = NEBENWERT_MAX_ABSTAND_S
|
||||
) -> float | None:
|
||||
"""Wie naechster_wert(), aber None statt eines beliebig weit entfernten
|
||||
Werts, wenn selbst der zeitlich nächste mehr als max_abstand_s von
|
||||
zielzeit entfernt liegt."""
|
||||
bester_wert = None
|
||||
beste_diff = None
|
||||
for ts, wert in verlauf:
|
||||
gezahlt = zahl(wert)
|
||||
if gezahlt is None:
|
||||
continue
|
||||
diff = abs((ts - zielzeit).total_seconds())
|
||||
if beste_diff is None or diff < beste_diff:
|
||||
bester_wert = gezahlt
|
||||
beste_diff = diff
|
||||
if beste_diff is None or beste_diff > max_abstand_s:
|
||||
return None
|
||||
return bester_wert
|
||||
|
||||
@@ -25,6 +25,16 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
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.
|
||||
_letzte_gueltige_spannung: dict[str, float | None] = {"wert": None}
|
||||
|
||||
|
||||
def zustand_oder_none(hass: HomeAssistant, entity_id: str | None) -> str | None:
|
||||
"""Sicherer Zustandszugriff.
|
||||
@@ -162,6 +172,14 @@ def fahrzeugstatus(hass: HomeAssistant, werte: Sensorzuordnung) -> dict:
|
||||
# sonst zeigt die "Zustand"-Kachel einen Wert, der im Messwertverlauf
|
||||
# nie auftaucht, weil er dort schon verworfen wird.
|
||||
spannung = None
|
||||
if spannung is not None:
|
||||
_letzte_gueltige_spannung["wert"] = spannung
|
||||
else:
|
||||
# Nutzerwunsch: statt "unbekannt" die zuletzt gemessene Spannung
|
||||
# zeigen, wenn der Sensor gerade nichts Plausibles liefert (z. B.
|
||||
# Dongle offline, meldet dann oft 0 V) - None bleibt nur, solange
|
||||
# noch nie ein gültiger Wert beobachtet wurde.
|
||||
spannung = _letzte_gueltige_spannung["wert"]
|
||||
|
||||
return {
|
||||
"km": _zu_zahl(zustand_oder_none(hass, werte.KM_SENSOR)),
|
||||
|
||||
Reference in New Issue
Block a user