From 9d954d6bd6ed86c640564074d7a56ae334a38e60 Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Tue, 25 Aug 2026 14:19:13 +0200 Subject: [PATCH] =?UTF-8?q?ShineBridge=20v1.9.6=20=E2=80=94=20Beispiel-Dia?= =?UTF-8?q?gramm=20im=20Leerzustand,=20Tages-/Wochen-Umschaltung=20an=2014?= =?UTF-8?q?=20Wochen=20gekn=C3=BCpft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Chart-Logik in buildCostChart() ausgelagert, wiederverwendbar für echte und Beispiel-Daten - Neu: bei Erstinstallation (noch keine echten Daten) zeigt die Finanzen-Seite jetzt ein klar als 'BEISPIEL' markiertes, entsättigtes Vorschau-Diagramm mit frei erfundenen (deterministischen) Werten statt nur eines Texthinweises — keine echt aussehenden Fantasiezahlen, bewusst gedämpfte Farben + Badge - Umschaltung Tage→Wochen jetzt an 14 Wochen (98 Tage) Historie geknüpft statt schon nach 21 Tagen — bis dahin rollierendes Tages-Fenster, danach rollierendes Wochen-Fenster (älteste Periode wandert raus sobald neue dazukommt) Co-Authored-By: Claude Sonnet 5 --- haos-addon/config.yaml | 2 +- haos-addon/src/web/index.html | 222 +++++++++++++++++++--------------- 2 files changed, 126 insertions(+), 98 deletions(-) diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index d87fb8c..ee223de 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.9.5" +version: "1.9.6" slug: shinebridge description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte url: https://gitea.bitfire.work/retr0/shinebridge diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 6efa598..c540942 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -736,6 +736,122 @@ function renderEnergy(inverters, aggregates, period, spotData) { // ── Finance Tab ─────────────────────────────────────────────── +// Erzeugt plausible, aber frei erfundene Tagesdaten fürs Beispiel-Diagramm im +// Leerzustand (Erstinstallation). Deterministisch (kein echter Zufall), damit +// die Vorschau bei jedem Laden gleich aussieht statt bei jedem Reload zu springen. +function exampleFinanceDays(n) { + const out = []; + const today = new Date(); + for (let i = n - 1; i >= 0; i--) { + const d = new Date(today); d.setDate(d.getDate() - i); + const wave = 4.2 + 2.6 * Math.sin(i * 0.6) + (i % 3 === 0 ? 1.4 : 0); + const fixed = Math.max(1.5, wave); + const spot = Math.max(0.8, fixed * (0.68 + 0.1 * Math.cos(i * 0.9))); + out.push({ date: d.toISOString().slice(0, 10), fixed_eur: +fixed.toFixed(2), spot_eur: +spot.toFixed(2) }); + } + return out; +} + +// Baut das Kosten-Balkendiagramm (echte oder Beispiel-Daten). Aggregiert ab +// 14 Wochen (98 Tagen) angesammelter Historie automatisch von Tages- auf +// Wochenbalken; zeigt davon je nach verfügbarer Breite die letzten 10–28 +// Perioden (rollierendes Fenster — ältere Balken wandern rechts raus, sobald +// neue dazukommen). +function buildCostChart(days, el, isExample) { + const C = isExample + ? { fixed: '#5b6472', spot: '#6b6155', grid: '#262b33' } + : { fixed: '#58a6ff', spot: '#f0883e', grid: '#30363d' }; + + const rawN = days.length; + const bucket = rawN >= 98 ? 7 : 1; + const allPeriods = bucket === 1 ? days.map(d => ({ + fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5), + })) : (() => { + const out = []; + for (let i = 0; i < days.length; i += bucket) { + const chunk = days.slice(i, i + bucket); + const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0); + const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null); + const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null; + out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) }); + } + return out; + })(); + + // Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite + // zeichnen (keine viewBox-Skalierung) — Höhe bleibt dabei fix, unabhängig von + // der Breite. Auf breiten Screens werden mehr Perioden gezeigt statt dieselben + // künstlich zu verbreitern. + const H = 240, PL = 40, PR = 8, PT = 14, PB = 46; + const measuredW = el.clientWidth || 380; + const W = Math.max(300, measuredW); + const cW = W - PL - PR; + const IDEAL_SLOT = 24; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah + const maxPeriods = Math.min(28, Math.max(10, Math.round(cW / IDEAL_SLOT))); + const periods = allPeriods.slice(-maxPeriods); + + const n = periods.length; + const cH = H - PT - PB; + const slot = cW / n; + const barW = Math.max(3, Math.min(26, slot * 0.36)); + const barGap = Math.max(1.5, slot * 0.08); + const rBar = Math.min(2.5, barW / 2); + + const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null)); + const maxVal = Math.max(...allVals, 0.01); + + const yTicks = 4; + let gridLines = '', yLabels = ''; + for (let i = 0; i <= yTicks; i++) { + const v = maxVal * i / yTicks; + const y = PT + cH - (cH * i / yTicks); + gridLines += ``; + yLabels += `${fEur(v)}`; + } + + const gradId = isExample ? 'Ex' : ''; + let bars = '', xLabels = ''; + periods.forEach((d, i) => { + const cx = PL + i * slot + slot / 2; + const hFixed = Math.max(0, cH * d.fixed_eur / maxVal); + bars += ``; + if (d.spot_eur != null) { + const hSpot = Math.max(0, cH * d.spot_eur / maxVal); + bars += ``; + } + const ly = H - PB + 12; + xLabels += `${d.label}`; + }); + + const defs = ` + + + + + + + + + `; + + const bucketLabel = bucket === 1 ? 'Tag' : 'Woche'; + const rangeNote = allPeriods.length > n ? ` · letzte ${n}` : ''; + const legend = `
+ Festpreis + Spot (hypothetisch) +
`; + + return ` +
+
Kosten je ${bucketLabel}${rangeNote}
+ + ${defs}${gridLines}${yLabels}${bars}${xLabels} + + ${legend} + ${isExample ? `
BEISPIEL
` : ''} +
`; +} + async function loadFinance() { const el = document.getElementById("finance-content"); let data; @@ -746,10 +862,14 @@ async function loadFinance() { const { days, fixed_total_eur, spot_total_eur, savings_eur, period_start, spot_days, total_days } = data; if (!days || days.length === 0) { - el.innerHTML = `
-
📊
-
Noch keine Daten
-
Der Tracker sammelt ab morgen täglich Daten.
Abrechnungsjahr ab ${period_start}.
+ const exampleChart = buildCostChart(exampleFinanceDays(14), el, true); + el.innerHTML = `
+
+
📊
+
Noch keine echten Daten
+
Der Tracker sammelt ab morgen täglich Daten.
Abrechnungsjahr ab ${period_start}. Unten siehst du, wie die Ansicht dann aussieht.
+
+ ${exampleChart}
`; return; } @@ -796,99 +916,7 @@ async function loadFinance() { ${spotCard}${savCard}
`; - // ── SVG-Balkendiagramm ──────────────────────────────────────── - // Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112 - // Tagesbalken lieber Wochenbalken. - const rawN = days.length; - const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1; - const allPeriods = bucket === 1 ? days.map(d => ({ - fixed_eur: d.fixed_eur, spot_eur: d.spot_eur, label: d.date.slice(5), - })) : (() => { - const out = []; - for (let i = 0; i < days.length; i += bucket) { - const chunk = days.slice(i, i + bucket); - const fixed = chunk.reduce((s, d) => s + (d.fixed_eur || 0), 0); - const spotVals = chunk.map(d => d.spot_eur).filter(v => v != null); - const spot = spotVals.length ? spotVals.reduce((a, b) => a + b, 0) : null; - out.push({ fixed_eur: fixed, spot_eur: spot, label: chunk[0].date.slice(5) }); - } - return out; - })(); - - // Container-Breite messen und die SVG in echten Pixeln in genau dieser Breite - // zeichnen (keine viewBox-Skalierung mehr) — Höhe bleibt dabei fix, unabhängig - // von der Breite. Statt auf breiten Screens dieselben 14 Balken künstlich - // aufzublasen, wird stattdessen die Anzahl gezeigter Perioden an die Breite - // angepasst — mehr Platz zeigt mehr Wochen, bei einer angenehmen Zielbreite - // pro Balkenpaar (~46px), nicht überbreite Einzelbalken. - const H = 240, PL = 40, PR = 8, PT = 14, PB = 46; - const measuredW = el.clientWidth || 380; - const W = Math.max(300, measuredW); - const cW = W - PL - PR; - const IDEAL_SLOT = 24; // px/Periode, entspricht dem Slot, der auf Handy-Breite gut aussah - const maxPeriods = Math.min(28, Math.max(10, Math.round(cW / IDEAL_SLOT))); - const periods = allPeriods.slice(-maxPeriods); - - const n = periods.length; - const cH = H - PT - PB; - const slot = cW / n; - const barW = Math.max(3, Math.min(26, slot * 0.36)); - const barGap = Math.max(1.5, slot * 0.08); - const rBar = Math.min(2.5, barW / 2); - - const allVals = periods.flatMap(d => [d.fixed_eur, d.spot_eur].filter(v => v != null)); - const maxVal = Math.max(...allVals, 0.01); - - const yTicks = 4; - let gridLines = '', yLabels = ''; - for (let i = 0; i <= yTicks; i++) { - const v = maxVal * i / yTicks; - const y = PT + cH - (cH * i / yTicks); - gridLines += ``; - yLabels += `${fEur(v)}`; - } - - // Labels schräg statt horizontal — braucht bei 14 Balken pro Label deutlich - // weniger Breite, dadurch bleiben trotz Platzmangel alle 14 lesbar. - let bars = '', xLabels = ''; - periods.forEach((d, i) => { - const cx = PL + i * slot + slot / 2; - const hFixed = Math.max(0, cH * d.fixed_eur / maxVal); - bars += ``; - if (d.spot_eur != null) { - const hSpot = Math.max(0, cH * d.spot_eur / maxVal); - bars += ``; - } - const ly = H - PB + 12; - xLabels += `${d.label}`; - }); - - const defs = ` - - - - - - - - - `; - - const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen'; - const rangeNote = allPeriods.length > n ? ` · letzte ${n}` : ''; - const legend = `
- Festpreis - Spot (hypothetisch) -
`; - - const chartSection = ` -
-
Kosten je ${bucketLabel}${rangeNote}
- - ${defs}${gridLines}${yLabels}${bars}${xLabels} - - ${legend} -
`; + const chartSection = buildCostChart(days, el, false); el.innerHTML = `
${empfehlung}${cards}${chartSection}
`; } catch(e) { el.innerHTML = '
Fehler beim Laden
'; console.error('loadFinance:', e); }