diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index 46aaf75..c46d7af 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.7.7" +version: "1.8.0" 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 e54d597..210ef80 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -545,6 +545,29 @@ def api_import_config(): threading.Thread(target=_restart_all, daemon=True).start() return jsonify({"ok": True, "inverters": len(inverters)}) +_spot_cache: Dict[str, Any] = {"ts": 0.0, "data": []} +_SPOT_TTL = 900 # 15 Minuten + +@app.get("/api/spot-price") +def api_spot_price(): + import urllib.request as _ur + global _spot_cache + now = time.time() + if now - _spot_cache["ts"] < _SPOT_TTL and _spot_cache["data"]: + return jsonify({"ok": True, "data": _spot_cache["data"]}) + try: + with _ur.urlopen("https://api.awattar.de/v1/marketdata", timeout=8) as r: + raw = json.loads(r.read()) + entries = [ + {"ts": int(d["start_timestamp"] // 1000), "price": round(d["marketprice"] / 10, 2)} + for d in raw.get("data", []) + ] + _spot_cache = {"ts": now, "data": entries} + return jsonify({"ok": True, "data": entries}) + except Exception as e: + log.warning("Spot-Price API Fehler: %s", e) + return jsonify({"ok": False, "data": _spot_cache.get("data", [])}) + @app.get("/") def index(): return send_from_directory(WEB_DIR, "index.html") diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index db6082e..5d22248 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -372,7 +372,57 @@ function switchTab(name) { // ── Energy Dashboard ────────────────────────────────────────── -function renderEnergy(inverters, aggregates, period) { +function renderSpotChart(spotData) { + if (!spotData || !spotData.length) return ''; + const now = Math.floor(Date.now() / 1000); + const startH = Math.floor(now / 3600) * 3600; + const hours = spotData.filter(d => d.ts >= startH && d.ts < startH + 86400).sort((a,b) => a.ts - b.ts); + if (hours.length < 2) return ''; + + const prices = hours.map(h => h.price); + const minP = Math.min(...prices), maxP = Math.max(...prices); + const rangeP = maxP - minP || 1; + const q1 = minP + rangeP / 3, q2 = minP + 2 * rangeP / 3; + const priceCol = p => p <= q1 ? '#3fb950' : p <= q2 ? '#f0c040' : '#f85149'; + + const W=520, H=108, PL=34, PR=8, PT=14, PB=22; + const cW=W-PL-PR, cH=H-PT-PB, bW=cW/hours.length; + + const bars = hours.map((h,i) => { + const isNow = now >= h.ts && now < h.ts+3600; + const barH = Math.max(2, ((h.price-minP)/rangeP)*cH); + const x=(PL+i*bW), y=PT+cH-barH; + const col = priceCol(h.price); + const hh = new Date(h.ts*1000).getHours(); + const tl = hh%6===0 ? `${String(hh).padStart(2,'0')}h` : ''; + const pl = isNow ? `${h.price.toFixed(1)}` : ''; + return ` + ${isNow?``:''} + ${tl}${pl}`; + }).join(''); + + const yTicks = [[minP,PT+cH],[maxP,PT]].map(([p,y]) => + ` + ${p.toFixed(0)}` + ).join(''); + + const cur = hours.find(h => now>=h.ts && now${cur.price.toFixed(2)} ct/kWhJetzt` + : ''; + + return `
+
+ EPEX SPOT · 24h + ${curLabel} +
+ + ${yTicks}${bars} + +
`; +} + +function renderEnergy(inverters, aggregates, period, spotData) { const el = document.getElementById("energy-content"); if (!aggregates || !Object.keys(aggregates).length) { el.innerHTML = '
Warte auf erste Messung…
'; @@ -536,8 +586,10 @@ function renderEnergy(inverters, aggregates, period) { const cards = sectionCards(mon.label || 'Diesen Monat', mon, C.imp) + sectionCards(yr.label || 'Dieses Jahr', yr, C.imp); + const spotHtml = renderSpotChart(spotData); + el.innerHTML = `
-
${svg}
+
${svg}${spotHtml}
${cards ? `
${cards}
` : ''}
`; } @@ -554,7 +606,8 @@ async function refreshData() { keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte"; renderLive(d.inverters || {}, d.aggregates || {}); const period = await fetchJSON(api("api/period-energy")).catch(() => ({})); - renderEnergy(d.inverters || {}, d.aggregates || {}, period); + const spot = await fetchJSON(api("api/spot-price")).catch(() => ({})); + renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || [])); } catch(e) { document.getElementById("pill-mqtt").className = "pill err"; }