From 11050687d0f2c7fdb294a7771d6ba358355b07db Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Tue, 28 Apr 2026 21:49:45 +0200 Subject: [PATCH] =?UTF-8?q?Redesign:=20Energie-Dashboard=20HA-Style=20?= =?UTF-8?q?=E2=80=94=20animateMotion=20Dots=20+=20Kreis-Nodes=20(v1.6.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kreisförmige Nodes wie im HA Energie-Dashboard (r=44, farbiger Rand) - Bezier-Kurven mit wandernden Punkten via animateMotion (3 Dots/Pfad) - Flussrichtung korrekt: reverse für Export, Entladen - Inaktive Pfade gedimmt, keine Dots - CSS flowFwd/flowBwd entfernt (nicht mehr benötigt) Co-Authored-By: Claude Sonnet 4.6 --- haos-addon/config.yaml | 2 +- haos-addon/src/web/index.html | 147 +++++++++++++++++++--------------- 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index ba5d346..bdf41ab 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.6.1" +version: "1.6.2" slug: shinebridge description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI url: https://gitea.bitfire.work/retr0/shinebridge diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 16b7c7b..43a275d 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -140,11 +140,7 @@ .kwh-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 8px; text-align: center; } .kwh-card .kv { font-size: 18px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; } .kwh-card .kl { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; margin-top: 4px; } - @keyframes flowFwd { to { stroke-dashoffset: -16; } } - @keyframes flowBwd { to { stroke-dashoffset: 16; } } - .flow-line { stroke-dasharray: 6 6; animation: flowFwd .7s linear infinite; } - .flow-line.reverse { animation: flowBwd .7s linear infinite; } - .flow-line.inactive { animation: none; opacity: .18; } + .flow-dot { /* dots via animateMotion — no CSS animation needed */ } /* Toast */ .toast { position: fixed; bottom: 24px; right: 24px; padding: 12px 20px; @@ -383,13 +379,13 @@ function renderEnergy(inverters, aggregates) { const houseW = Math.max(0, pvW + gridImport + batDchW - batChW - evW); const hasBat = batSoc !== undefined && batSoc !== null; - const THRESH = 20; - const pvOn = pvW > THRESH; - const impOn = gridImport > THRESH; - const expOn = gridExport > THRESH; - const chOn = batChW > THRESH; - const dchOn = batDchW > THRESH; - const evOn = evW > THRESH; + const T = 20; + const pvOn = pvW > T; + const impOn = gridImport > T; + const expOn = gridExport > T; + const chOn = batChW > T; + const dchOn = batDchW > T; + const evOn = evW > T; const C = { pv:'#f0c040', imp:'#f85149', exp:'#3fb950', @@ -399,88 +395,109 @@ function renderEnergy(inverters, aggregates) { function fW(w) { const a = Math.abs(w); - if (a < 5) return '— W'; - if (a >= 1000) return (a/1000).toFixed(2) + ' kW'; + if (a < 5) return '—'; + if (a >= 1000) return (a / 1000).toFixed(2) + ' kW'; return Math.round(a) + ' W'; } - // SVG path icons (20×20 canvas, all stroke-based) + // SVG stroke icons on 20×20 canvas const ICONS = { - solar: ` - `, - grid: ``, - house: ` - - `, - bat: ` - `, - ev: ``, + solar:` + `, + grid: ``, + house:` + + `, + bat: ` + `, + ev: ``, }; - function icon(name, cx, cy, col) { - const sz = 18; - return `${ICONS[name]}`; + function icon(name, cx, cy, col, sz) { + sz = sz || 22; + const s = (sz / 20).toFixed(4); + return `${ICONS[name]}`; } - // Node: rounded rect 106×62 - function node(cx, cy, iconName, label, valW, col, active, sublabel) { - const c = active ? col : C.dim; - const sw = active ? 1.5 : 1; + // Circle node — HA style + const R = 44; + function node(cx, cy, iconName, topLabel, valW, col, active, sub) { + const c = active ? col : C.dim; + const sw = active ? 2 : 1; + const fi = active ? '0.08' : '0.03'; return ` - - ${icon(iconName, cx, cy-12, c)} - ${fW(valW)} - ${label}${sublabel?' '+sublabel:''} + + ${icon(iconName, cx, cy - 11, c, 22)} + ${fW(valW)} + ${topLabel}${sub?' · '+sub:''} `; } - // Bezier flow path - function flow(d, col, active, reverse) { - if (!active) return ``; - return ``; + // Animated dots along a named path (HA-style) + function flowDots(pid, col, on, reverse, spd) { + if (!on) return ''; + spd = spd || 1.6; + const kp = reverse ? '1;0' : '0;1'; + return [0, spd/3, 2*spd/3].map(b => + ` + + + + ` + ).join(''); } - // Paths (bezier curves between node edges) - const pPV = `M 130,80 C 130,116 260,108 260,132`; - const pGrid = `M 390,80 C 390,116 260,108 260,132`; - const pBat = `M 207,162 C 165,162 130,204 130,236`; - const pEV = `M 313,162 C 355,162 390,204 390,236`; + // Path segments (bezier from node-edge to node-edge) + // Solar(145,75) bottom→ House(260,180) top; Grid(375,75) bottom→ House top + // House bottom → Battery(145,292) top; House bottom → EV(375,292) top + const SEG = [ + { id:'ep-pv', d:`M 145,119 C 145,155 260,155 260,136`, col:C.pv, on:pvOn, rev:false }, + { id:'ep-grid', d:`M 375,119 C 375,155 260,155 260,136`, col:impOn?C.imp:C.exp, on:impOn||expOn, rev:expOn }, + { id:'ep-bat', d:`M 245,224 C 215,260 145,260 145,248`, col:C.bat, on:hasBat&&(chOn||dchOn), rev:dchOn }, + { id:'ep-ev', d:`M 275,224 C 305,260 375,260 375,248`, col:C.ev, on:evOn, rev:false }, + ]; + + const activeSeg = SEG.filter(s => hasBat || s.id !== 'ep-bat'); + + const defs = activeSeg.map(s => ``).join(''); + const lines = activeSeg.map(s => + `` + ).join(''); + const dotsSvg = activeSeg.map(s => flowDots(s.id, s.col, s.on, s.rev)).join(''); const gridCol = impOn ? C.imp : expOn ? C.exp : C.dim; - const gridVal = impOn ? gridImport : expOn ? gridExport : 0; const gridLbl = impOn ? 'NETZBEZUG' : expOn ? 'EINSPEISUNG' : 'NETZ'; + const gridVal = impOn ? gridImport : gridExport; const batVal = chOn ? batChW : dchOn ? batDchW : 0; const batLbl = chOn ? 'LADEN' : dchOn ? 'ENTLADEN' : 'BATTERIE'; - const batSub = hasBat ? Math.round(batSoc)+'%' : ''; + const batSub = hasBat ? Math.round(batSoc) + '%' : ''; - const svgH = 310; - const svg = ` - ${flow(pPV, C.pv, pvOn, false)} - ${flow(pGrid, gridCol, impOn||expOn, expOn)} - ${hasBat ? flow(pBat, C.bat, chOn||dchOn, dchOn) : ''} - ${flow(pEV, C.ev, evOn, false)} - ${node(130, 50, 'solar', 'SOLAR', pvW, C.pv, pvOn, '')} - ${node(390, 50, 'grid', gridLbl, gridVal, gridCol, impOn||expOn, '')} - ${node(260, 162, 'house', 'HAUS', houseW, C.txt, true, '')} - ${hasBat ? node(130, 265, 'bat', batLbl, batVal, C.bat, chOn||dchOn, batSub) : ''} - ${node(390, 265, 'ev', 'WALLBOX', evW, C.ev, evOn, '')} + const svg = ` + ${defs} + ${lines} + ${dotsSvg} + ${node(145, 75, 'solar', 'SOLAR', pvW, C.pv, pvOn, '')} + ${node(375, 75, 'grid', gridLbl, gridVal, gridCol, impOn||expOn, '')} + ${node(260, 180, 'house', 'HAUS', houseW, C.txt, true, '')} + ${hasBat ? node(145, 292, 'bat', batLbl, batVal, C.bat, chOn||dchOn, batSub) : ''} + ${node(375, 292, 'ev', 'WALLBOX', evW, C.ev, evOn, '')} `; function kwhCard(label, val, col) { - if (val === undefined || val === null) return ''; - const d = val >= 100 ? val.toFixed(0) : val >= 10 ? val.toFixed(1) : val.toFixed(2); + if (val == null) return ''; + const d = val >= 100 ? val.toFixed(0) : val.toFixed(val >= 10 ? 1 : 2); return `
-
${d} kWh
+
${d} kWh
${label}
`; } const cards = [ - kwhCard('PV Heute', aggregates.total_energy_today, C.pv), - kwhCard('Netzbezug', aggregates.grid_import_kwh, C.imp), - kwhCard('Einspeisung', aggregates.grid_export_kwh, C.exp), - kwhCard('Bat. Laden', aggregates.bat_charge_total, C.bat), - kwhCard('Bat. Entladen', aggregates.bat_discharge_total, C.bat), + kwhCard('PV Heute', aggregates.total_energy_today, C.pv), + kwhCard('Netzbezug', aggregates.grid_import_kwh, C.imp), + kwhCard('Einspeisung', aggregates.grid_export_kwh, C.exp), + kwhCard('Bat. Laden', aggregates.bat_charge_total, C.bat), + kwhCard('Bat. Entladen', aggregates.bat_discharge_total, C.bat), ].filter(Boolean).join(''); el.innerHTML = `