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 = `