From 13de5ff976f578fcdb06a13cfb9a186b5c90bdd6 Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Wed, 29 Apr 2026 07:35:48 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Energie-Dashboard=20Nodes=20gr=C3=B6?= =?UTF-8?q?=C3=9Fer=20(R=3D56),=20Labels=20aus=20API,=20Abrechnungsperiode?= =?UTF-8?q?=20konfig.=20(v1.7.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SVG-Nodes: Radius 44 → 56, Icon 22 → 26px, Abstände neu berechnet - Segment-Pfade an neue Positionen angepasst (40px Abstand Kante→Kante) - period.monthly.label / yearly.label statt hardcoded "Diesen Monat" / "Dieses Jahr" - billing_day/billing_month: history.period_key(), /api/period-energy, Settings-UI Co-Authored-By: Claude Sonnet 4.6 --- haos-addon/config.yaml | 2 +- haos-addon/src/history.py | 18 +++++++++-- haos-addon/src/main.py | 38 +++++++++++++++++++--- haos-addon/src/web/index.html | 59 +++++++++++++++++++++-------------- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index 565b5da..f82e9aa 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.7.0" +version: "1.7.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/history.py b/haos-addon/src/history.py index 18272c8..080c648 100644 --- a/haos-addon/src/history.py +++ b/haos-addon/src/history.py @@ -52,10 +52,22 @@ def init_db(): log.info("History DB initialisiert: %s", DB_PATH) -def period_key(period_type: str) -> str: +def period_key(period_type: str, billing_day: int = 1, billing_month: int = 1) -> str: import datetime - now = datetime.date.today() - return now.strftime("%Y-%m") if period_type == "monthly" else now.strftime("%Y") + today = datetime.date.today() + if period_type == "monthly": + return today.strftime("%Y-%m") + # Jahresperiode: Beginn = letzter Abrechnungsstichtag + try: + start = datetime.date(today.year, billing_month, billing_day) + except ValueError: + start = datetime.date(today.year, billing_month, 1) + if today < start: + try: + start = datetime.date(today.year - 1, billing_month, billing_day) + except ValueError: + start = datetime.date(today.year - 1, billing_month, 1) + return start.isoformat() # z.B. "2025-04-01" def save_period_start_if_new(agg_id: str, period_type: str, key: str, current_value: float): diff --git a/haos-addon/src/main.py b/haos-addon/src/main.py index 3a60831..e54d597 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -88,6 +88,8 @@ def _defaults() -> Dict[str, Any]: "mqtt_pass": "", "price_import": 0.30, "price_export": 0.08, + "billing_day": 1, + "billing_month": 1, "inverters": [], } @@ -118,6 +120,8 @@ def save_config(): "mqtt_pass": State.mqtt_cfg.get("mqtt_pass", ""), "price_import": State.mqtt_cfg.get("price_import", 0.30), "price_export": State.mqtt_cfg.get("price_export", 0.08), + "billing_day": State.mqtt_cfg.get("billing_day", 1), + "billing_month": State.mqtt_cfg.get("billing_month", 1), "inverters": State.inverters_cfg, } with open(CONFIG_PATH, "w") as f: @@ -345,6 +349,9 @@ def api_save_config(): for k in ("price_import", "price_export"): if k in data: State.mqtt_cfg[k] = float(data[k]) + for k in ("billing_day", "billing_month"): + if k in data: + State.mqtt_cfg[k] = int(data[k]) save_config() threading.Thread(target=_restart_all, daemon=True).start() return jsonify({"ok": True}) @@ -352,16 +359,37 @@ def api_save_config(): @app.get("/api/period-energy") def api_period_energy(): + import datetime agg = _compute_aggregates() - price_import = float(State.mqtt_cfg.get("price_import", 0.30)) - price_export = float(State.mqtt_cfg.get("price_export", 0.08)) + price_import = float(State.mqtt_cfg.get("price_import", 0.30)) + price_export = float(State.mqtt_cfg.get("price_export", 0.08)) + billing_day = int(State.mqtt_cfg.get("billing_day", 1)) + billing_month = int(State.mqtt_cfg.get("billing_month", 1)) - result = {"price_import": price_import, "price_export": price_export} + result = { + "price_import": price_import, + "price_export": price_export, + "billing_day": billing_day, + "billing_month": billing_month, + } + + MONTHS_DE = ["","Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"] for period_type in ("monthly", "yearly"): - key = history.period_key(period_type) + key = history.period_key(period_type, billing_day, billing_month) entry = {} - for agg_id in ("grid_import_kwh", "grid_export_kwh", "total_energy_today"): + + # Lesbare Beschriftung + if period_type == "monthly": + d = datetime.date.fromisoformat(key + "-01") + entry["label"] = f"{MONTHS_DE[d.month]} {d.year}" + else: + d = datetime.date.fromisoformat(key) + end = datetime.date(d.year + 1, billing_month, billing_day) if billing_month != 1 or billing_day != 1 \ + else datetime.date(d.year + 1, 1, 1) + entry["label"] = f"{d.strftime('%d.%m.%Y')} – {(end - datetime.timedelta(days=1)).strftime('%d.%m.%Y')}" + + for agg_id in ("grid_import_kwh", "grid_export_kwh"): cur = agg.get(agg_id) if cur is None: continue diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 89cea89..abbc2a1 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -213,7 +213,16 @@
- +
+
+ + . + + (Tag . Monat) +
+
z.B. 1 . 4 = 01. April
+
+
@@ -424,22 +433,22 @@ function renderEnergy(inverters, aggregates, period) { }; function icon(name, cx, cy, col, sz) { - sz = sz || 22; + sz = sz || 26; const s = (sz / 20).toFixed(4); return `${ICONS[name]}`; } // Circle node — HA style - const R = 44; + const R = 56; 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 - 11, c, 22)} - ${fW(valW)} - ${topLabel}${sub?' · '+sub:''} + ${icon(iconName, cx, cy - 15, c, 26)} + ${fW(valW)} + ${topLabel}${sub?' · '+sub:''} `; } @@ -457,15 +466,13 @@ function renderEnergy(inverters, aggregates, period) { ).join(''); } - // 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 - // Kreuz-Layout: Solar oben, Grid links, Haus Mitte, Batterie rechts, Wallbox unten (optional) + // Kreuz-Layout: Solar(260,72) oben, Grid(108,224) links, Haus(260,224) Mitte, + // Batterie(412,224) rechts, Wallbox(260,376) unten (optional). R=56, Abstand=40px. const SEG = [ - { id:'ep-pv', d:`M 260,121 C 258,138 262,146 260,158`, col:C.pv, on:pvOn, rev:false }, - { id:'ep-grid', d:`M 140,200 C 162,197 192,203 214,200`, col:impOn?C.imp:C.exp, on:impOn||expOn, rev:expOn }, - { id:'ep-bat', d:`M 306,200 C 330,197 356,203 380,200`, col:C.bat, on:chOn||dchOn, rev:dchOn }, - ...(hasEV ? [{ id:'ep-ev', d:`M 260,244 C 258,261 262,269 260,279`, col:C.ev, on:evOn, rev:false }] : []), + { id:'ep-pv', d:`M 260,128 C 258,148 262,152 260,168`, col:C.pv, on:pvOn, rev:false }, + { id:'ep-grid', d:`M 164,224 C 183,220 185,228 204,224`, col:impOn?C.imp:C.exp, on:impOn||expOn, rev:expOn }, + { id:'ep-bat', d:`M 316,224 C 335,220 337,228 356,224`, col:C.bat, on:chOn||dchOn, rev:dchOn }, + ...(hasEV ? [{ id:'ep-ev', d:`M 260,280 C 258,300 262,304 260,320`, col:C.ev, on:evOn, rev:false }] : []), ]; const defs = SEG.map(s => ``).join(''); @@ -481,16 +488,16 @@ function renderEnergy(inverters, aggregates, period) { const batLbl = chOn ? 'LADEN' : dchOn ? 'ENTLADEN' : 'BATTERIE'; const batSub = batSoc != null ? Math.round(batSoc) + '%' : ''; - const svgH = hasEV ? 410 : 278; + const svgH = hasEV ? 460 : 310; const svg = ` ${defs} ${lines} ${dotsSvg} - ${node(260, 77, 'solar', 'SOLAR', pvW, C.pv, pvOn, '')} - ${node( 95, 200, 'grid', gridLbl, gridVal, gridCol, impOn||expOn, '')} - ${node(260, 200, 'house', 'HAUS', houseW, C.txt, true, '')} - ${node(425, 200, 'bat', batLbl, batVal, C.bat, chOn||dchOn, batSub)} - ${hasEV ? node(260, 323, 'ev', 'WALLBOX', evW, C.ev, evOn, '') : ''} + ${node(260, 72, 'solar', 'SOLAR', pvW, C.pv, pvOn, '')} + ${node(108, 224, 'grid', gridLbl, gridVal, gridCol, impOn||expOn, '')} + ${node(260, 224, 'house', 'HAUS', houseW, C.txt, true, '')} + ${node(412, 224, 'bat', batLbl, batVal, C.bat, chOn||dchOn, batSub)} + ${hasEV ? node(260, 376, 'ev', 'WALLBOX', evW, C.ev, evOn, '') : ''} `; period = period || {}; @@ -525,8 +532,8 @@ function renderEnergy(inverters, aggregates, period) {
${imp}${exp}
`; } - const cards = sectionCards('Diesen Monat', mon, C.imp) + - sectionCards('Dieses Jahr', yr, C.imp); + const cards = sectionCards(mon.label || 'Diesen Monat', mon, C.imp) + + sectionCards(yr.label || 'Dieses Jahr', yr, C.imp); el.innerHTML = `
${svg}
@@ -768,12 +775,16 @@ async function loadSettings() { document.getElementById("cfg-mqtt-user").value = globalConfig.mqtt_user || ""; document.getElementById("cfg-price-import").value = globalConfig.price_import ?? 0.30; document.getElementById("cfg-price-export").value = globalConfig.price_export ?? 0.08; + document.getElementById("cfg-billing-day").value = globalConfig.billing_day ?? 1; + document.getElementById("cfg-billing-month").value = globalConfig.billing_month ?? 1; } async function savePrices() { const body = { - price_import: parseFloat(document.getElementById("cfg-price-import").value), - price_export: parseFloat(document.getElementById("cfg-price-export").value), + price_import: parseFloat(document.getElementById("cfg-price-import").value), + price_export: parseFloat(document.getElementById("cfg-price-export").value), + billing_day: parseInt(document.getElementById("cfg-billing-day").value), + billing_month: parseInt(document.getElementById("cfg-billing-month").value), }; try { await fetchJSON(api("api/config"), {