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 <[email protected]>
This commit is contained in:
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user