diff --git a/README.md b/README.md index afe7444..188bdcb 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ Konfigurierbar pro Gerät im Web UI: > > Die EMS-Funktion muss zusätzlich in der Kathrein easyOperate-App aktiviert sein. +Die Solarstrahlungs-Prognose wird als eigenes 24h-Balkendiagramm im Energie-Dashboard angezeigt, direkt unter dem EPEX-Spot-Chart (nur sichtbar mit gesetztem Standort). + **Prognosebasierter Zwangslade-Timeout (optional):** Mit Standort (Breitengrad/Längengrad, Einstellungen-Tab) passt sich der Zwangslade-Timeout an die Solarstrahlungs-Prognose (Open-Meteo, kostenlos) an — bei bald zu erwartender Sonne wird länger gewartet (gedeckelt), bei aussichtsloser Prognose eher vom Netz geladen statt sinnlos den vollen Timeout abzuwarten. Ohne Standort unverändertes Verhalten (fester Timeout). **Batterie-Schutz beim PV-Überschussladen:** Bei Anlagen mit Hausbatterie (z. B. Goodwe) wird die aktuelle Batterie-Entladeleistung vom berechneten PV-Überschuss abgezogen — ein Netzexport, der eigentlich aus dem Speicher kommt (z. B. bei bewölktem Wetter), wird so nicht fälschlich als PV-Überschuss fürs Auto verwendet. Betrifft nur PV-Überschussladen, Zwangsladen bleibt unverändert netzbasiert. diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index 19a6a6c..ed25b74 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.12.1" +version: "1.12.2" slug: shinebridge description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte url: https://gitea.bitfire.work/retr0/shinebridge diff --git a/haos-addon/src/weather_forecast.py b/haos-addon/src/weather_forecast.py index fe203d9..dbe268c 100644 --- a/haos-addon/src/weather_forecast.py +++ b/haos-addon/src/weather_forecast.py @@ -111,6 +111,9 @@ def summary(lat: Optional[float], lon: Optional[float], base_timeout_h: float = else: verdict = "unverändert" + # Naechste 24h fuers Diagramm im Dashboard (unter dem Strompreis-Chart) + horizon_24h = [{"ts": ts, "ghi": round(ghi, 0)} for ts, ghi in hourly if now <= ts < now + 24 * 3600] + return { "configured": True, "next_3h_max_ghi": next_3h_max, @@ -119,4 +122,5 @@ def summary(lat: Optional[float], lon: Optional[float], base_timeout_h: float = "example_base_h": base_timeout_h, "example_adjusted_h": round(adjusted, 2), "verdict": verdict, + "hourly": horizon_24h, } diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 7ea8ceb..9fdc51e 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -554,6 +554,51 @@ function renderSpotChart(spotData) { `; } +function renderWeatherChart(weather) { + if (!weather || !weather.configured || weather.error) return ''; + const hourly = weather.hourly || []; + if (hourly.length < 2) return ''; + + const now = Math.floor(Date.now() / 1000); + const maxGhi = Math.max(...hourly.map(h => h.ghi), 100); + const sunCol = g => g >= weather.sun_threshold ? '#f0c040' : g > 0 ? '#5b6472' : '#30363d'; + + const W=520, H=108, PL=34, PR=8, PT=14, PB=22; + const cW=W-PL-PR, cH=H-PT-PB, bW=cW/hourly.length; + + const bars = hourly.map((h,i) => { + const isNow = now >= h.ts && now < h.ts+3600; + const barH = Math.max(2, (h.ghi/maxGhi)*cH); + const x=(PL+i*bW), y=PT+cH-barH; + const col = sunCol(h.ghi); + const hh = new Date(h.ts*1000).getHours(); + const tl = hh%6===0 ? `${String(hh).padStart(2,'0')}h` : ''; + return ` + ${isNow?``:''} + ${tl}`; + }).join(''); + + const yTicks = [[0,PT+cH],[maxGhi,PT]].map(([g,y]) => + ` + ${g.toFixed(0)}` + ).join(''); + + const cur = hourly.find(h => now>=h.ts && now${cur.ghi.toFixed(0)} W/m²Jetzt` + : ''; + + return `
+
+ Solarprognose · 24h + ${curLabel} +
+ + ${yTicks}${bars} + +
`; +} + function fEur(v) { if (v == null) return null; return v >= 100 ? v.toFixed(0) + ' €' : v.toFixed(2) + ' €'; @@ -563,7 +608,7 @@ function fKwh(v) { return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + ' kWh'; } -function renderEnergy(inverters, aggregates, period, spotData) { +function renderEnergy(inverters, aggregates, period, spotData, weatherData) { const el = document.getElementById("energy-content"); if (!aggregates || !Object.keys(aggregates).length) { el.innerHTML = '
Warte auf erste Messung…
'; @@ -729,6 +774,7 @@ function renderEnergy(inverters, aggregates, period, spotData) { sectionCards(yr.label || 'Dieses Jahr', yr); const spotHtml = (period.spot_chart !== false) ? renderSpotChart(spotData) : ''; + const weatherHtml = renderWeatherChart(weatherData); const bt = period.billing_tracker; const trackerHtml = bt ? (() => { @@ -764,7 +810,7 @@ function renderEnergy(inverters, aggregates, period, spotData) { })() : ''; el.innerHTML = `
-
${svg}${spotHtml}
+
${svg}${spotHtml}${weatherHtml}
${cards ? `
${cards}
` : ''} ${trackerHtml}
`; @@ -972,9 +1018,10 @@ async function refreshData() { document.getElementById("subtitle").textContent = keys.length ? `${keys.length} Gerät${keys.length !== 1 ? "e" : ""}` : "Keine Geräte"; renderLive(d.inverters || {}, d.aggregates || {}, surplusData); - const period = await fetchJSON(api("api/period-energy")).catch(() => ({})); - const spot = await fetchJSON(api("api/spot-price")).catch(() => ({})); - renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || [])); + const period = await fetchJSON(api("api/period-energy")).catch(() => ({})); + const spot = await fetchJSON(api("api/spot-price")).catch(() => ({})); + const weather = await fetchJSON(api("api/weather-forecast")).catch(() => ({})); + renderEnergy(d.inverters || {}, d.aggregates || {}, period, (spot.data || []), weather); } catch(e) { document.getElementById("pill-mqtt").className = "pill err"; }