diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index bf7ffb8..ba5d346 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.6.0" +version: "1.6.1" 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/main.py b/haos-addon/src/main.py index 158de5a..22c4b86 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -229,11 +229,14 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event): t0 = time.time() values = reader.read(inverter) - # Growatt-Proxy: grid_power (positiv=Netzbezug) aus power_to_grid ableiten - # wenn kein dedizierter Grid-Meter vorhanden (Goodwe setzt grid_power direkt) + # Growatt-Proxy: grid_power aus power_to_grid (nur Einspeisung bekannt) if values and "grid_power" not in values and "power_to_grid" in values: values["grid_power"] = -values["power_to_grid"] + # SDM-630-Proxy: total_power = Netzleistung (positiv=Bezug, negativ=Einspeisung) + if values and "grid_power" not in values and "import_kwh" in values and "total_power" in values: + values["grid_power"] = values["total_power"] + # EMS: PV-Überschuss aus anderen Geräten holen und Ladestrom regeln if ems is not None and values is not None: pv_surplus = _get_pv_surplus() diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index ea8a354..16b7c7b 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -365,11 +365,11 @@ function renderEnergy(inverters, aggregates) { return; } - const pvW = aggregates.total_pv_power || 0; - const gridW = aggregates.grid_power || 0; // + = Bezug, - = Einspeisung - const batChW = aggregates.bat_charge_power || 0; - const batDchW = aggregates.bat_discharge_power|| 0; - const batSoc = aggregates.bat_soc; + const pvW = aggregates.total_pv_power || 0; + const gridW = aggregates.grid_power || 0; + const batChW = aggregates.bat_charge_power || 0; + const batDchW = aggregates.bat_discharge_power || 0; + const batSoc = aggregates.bat_soc; let evW = 0; Object.entries(inverters).forEach(([id, inv]) => { @@ -380,78 +380,112 @@ function renderEnergy(inverters, aggregates) { const gridImport = Math.max(0, gridW); const gridExport = Math.max(0, -gridW); - const houseW = Math.max(0, pvW + gridImport + batDchW - batChW - evW); + 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 C = { + pv:'#f0c040', imp:'#f85149', exp:'#3fb950', + bat:'#bc8cff', ev:'#58a6ff', txt:'#e6edf3', + dim:'#8b949e', brd:'#30363d', s2:'#21262d', + }; function fW(w) { - if (Math.abs(w) < 1) return '0 W'; - if (Math.abs(w) >= 1000) return (w/1000).toFixed(2) + ' kW'; - return Math.round(w) + ' W'; + const a = Math.abs(w); + if (a < 5) return '— W'; + if (a >= 1000) return (a/1000).toFixed(2) + ' kW'; + return Math.round(a) + ' W'; } - function node(cx, cy, icon, label, value, col, extra) { + // SVG path icons (20×20 canvas, all stroke-based) + const ICONS = { + solar: ` + `, + grid: ``, + house: ` + + `, + bat: ` + `, + ev: ``, + }; + + function icon(name, cx, cy, col) { + const sz = 18; + 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; return ` - - ${icon} - ${fW(value)} - ${label} - ${extra||''} + + ${icon(iconName, cx, cy-12, c)} + ${fW(valW)} + ${label}${sublabel?' '+sublabel:''} `; } - const C = { acc:'var(--accent)', red:'var(--red)', grn:'var(--green)', pur:'var(--purple)', blu:'var(--blue)', dim:'var(--text-dim)', brd:'var(--border)' }; - - // Flow line helpers - function fline(x1,y1,x2,y2, col, cls) { - return ``; + // Bezier flow path + function flow(d, col, active, reverse) { + if (!active) return ``; + return ``; } - const hasBat = batSoc !== undefined && batSoc !== null; + // 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`; - const svg = ` - - ${fline(168,78,237,126, pvW>10?C.acc:C.brd, pvW>10?'':'inactive')} + 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 batVal = chOn ? batChW : dchOn ? batDchW : 0; + const batLbl = chOn ? 'LADEN' : dchOn ? 'ENTLADEN' : 'BATTERIE'; + const batSub = hasBat ? Math.round(batSoc)+'%' : ''; - - ${fline(372,78,303,126, - gridImport>10?C.red:gridExport>10?C.grn:C.brd, - gridExport>10?'reverse':gridImport>10?'':'inactive')} - - ${hasBat ? ` - ${fline(237,170,164,222, batChW>10||batDchW>10?C.pur:C.brd, batDchW>10?'reverse':batChW>10?'':'inactive')}` : ''} - - - ${fline(303,170,376,222, evW>10?C.blu:C.brd, evW>10?'':'inactive')} - - - ${node(135, 60, '☀️', 'Solar', pvW, C.acc)} - ${node(405, 60, '⚡', gridImport>10?'Netzbezug':gridExport>10?'Einspeisung':'Netz', - gridImport>10?gridImport:gridExport, gridImport>10?C.red:gridExport>10?C.grn:C.dim)} - ${node(270, 148, '🏠', 'Haus', houseW, 'var(--text)')} - ${hasBat ? node(135, 244, '🔋', `Batt. ${Math.round(batSoc)}%`, - batChW>batDchW?batChW:batDchW, C.pur) : ''} - ${node(405, 244, '🚗', 'Wallbox', evW, evW>10?C.blu:C.dim)} + 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, '')} `; - function kwhCard(icon, label, val, col) { + 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); - return `
-
${d}
-
${icon} ${label}
+ return `
+
${d} kWh
+
${label}
`; } const cards = [ - kwhCard('☀️','PV Heute kWh', aggregates.total_energy_today, C.acc), - kwhCard('📥','Bezug kWh', aggregates.grid_import_kwh, C.red), - kwhCard('📤','Einspeisung kWh', aggregates.grid_export_kwh, C.grn), - kwhCard('⚡','Bat. Laden kWh', aggregates.bat_charge_total, C.pur), - kwhCard('🔋','Bat. Entl. kWh', aggregates.bat_discharge_total, C.pur), + 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 = `
${svg}
- ${cards ? `
${cards}
` : ''} + ${cards ? `
${cards}
` : ''}
`; }