Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce98df596 |
@@ -1,5 +1,5 @@
|
||||
name: ShineBridge
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
slug: shinebridge
|
||||
description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte
|
||||
url: https://gitea.bitfire.work/retr0/shinebridge
|
||||
|
||||
@@ -511,6 +511,19 @@ def _restart_all():
|
||||
|
||||
# ── REST API ──────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/weather-forecast")
|
||||
def api_weather_forecast():
|
||||
with State.lock:
|
||||
lat = State.mqtt_cfg.get("site_lat")
|
||||
lon = State.mqtt_cfg.get("site_lon")
|
||||
# Beispiel-Basiswert: erstes konfigurierte Kathrein-Timeout, sonst 2h-Default
|
||||
base = next(
|
||||
(inv.get("ems_timeout") for inv in State.inverters_cfg if inv.get("ems_timeout")),
|
||||
2.0,
|
||||
)
|
||||
return jsonify(weather_forecast.summary(lat, lon, float(base)))
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
def api_get_config():
|
||||
with State.lock:
|
||||
|
||||
@@ -84,3 +84,39 @@ def adjusted_timeout_h(base_timeout_h: float, lat: Optional[float], lon: Optiona
|
||||
# nicht sinnlos den vollen Timeout abwarten, aber nie unter 0.5h
|
||||
return max(base_timeout_h * 0.5, 0.5)
|
||||
return base_timeout_h
|
||||
|
||||
|
||||
def summary(lat: Optional[float], lon: Optional[float], base_timeout_h: float = 2.0) -> dict:
|
||||
"""Für die UI: aktueller Prognose-Stand + Beispiel, wie sich ein Timeout
|
||||
dadurch veraendern wuerde. Nicht fuers eigentliche EMS-Update genutzt
|
||||
(das ruft adjusted_timeout_h direkt auf) — nur zur Anzeige/Diagnose."""
|
||||
if lat is None or lon is None:
|
||||
return {"configured": False}
|
||||
|
||||
hourly = get_hourly_ghi(lat, lon)
|
||||
if not hourly:
|
||||
return {"configured": True, "error": "Prognose aktuell nicht abrufbar"}
|
||||
|
||||
now = time.time()
|
||||
near = [ghi for ts, ghi in hourly if now <= ts < now + 3 * 3600]
|
||||
horizon = [ghi for ts, ghi in hourly if now <= ts < now + 12 * 3600]
|
||||
next_3h_max = round(max(near), 0) if near else 0
|
||||
horizon_12h_max = round(max(horizon), 0) if horizon else 0
|
||||
|
||||
adjusted = adjusted_timeout_h(base_timeout_h, lat, lon)
|
||||
if adjusted > base_timeout_h:
|
||||
verdict = "verlängert"
|
||||
elif adjusted < base_timeout_h:
|
||||
verdict = "verkürzt"
|
||||
else:
|
||||
verdict = "unverändert"
|
||||
|
||||
return {
|
||||
"configured": True,
|
||||
"next_3h_max_ghi": next_3h_max,
|
||||
"horizon_12h_max_ghi": horizon_12h_max,
|
||||
"sun_threshold": SUN_THRESHOLD,
|
||||
"example_base_h": base_timeout_h,
|
||||
"example_adjusted_h": round(adjusted, 2),
|
||||
"verdict": verdict,
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
<input type="number" id="cfg-site-lon" step="0.0001" placeholder="z.B. 13.4050"></div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="saveLocation()">Speichern</button>
|
||||
<div id="weather-preview" style="margin-top:.75rem;font-size:.85rem"></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
@@ -1252,6 +1253,7 @@ async function loadSettings() {
|
||||
document.getElementById("cfg-backup-retention").value = cfg.config_backup_retention_days ?? 14;
|
||||
document.getElementById("cfg-site-lat").value = cfg.site_lat ?? "";
|
||||
document.getElementById("cfg-site-lon").value = cfg.site_lon ?? "";
|
||||
loadWeatherPreview();
|
||||
}
|
||||
|
||||
async function saveLocation() {
|
||||
@@ -1263,9 +1265,30 @@ async function saveLocation() {
|
||||
body: JSON.stringify({ site_lat: lat === "" ? null : lat, site_lon: lon === "" ? null : lon }),
|
||||
});
|
||||
showToast("Gespeichert!", "ok");
|
||||
setTimeout(loadWeatherPreview, 500);
|
||||
} catch(e) { showToast("Fehler beim Speichern", "err"); }
|
||||
}
|
||||
|
||||
async function loadWeatherPreview() {
|
||||
const el = document.getElementById("weather-preview");
|
||||
let data;
|
||||
try { data = await fetchJSON(api("api/weather-forecast")); }
|
||||
catch(e) { el.innerHTML = ""; return; }
|
||||
|
||||
if (!data.configured) {
|
||||
el.innerHTML = `<span style="color:var(--text-dim)">Kein Standort gesetzt — Zwangslade-Timeout bleibt fest.</span>`;
|
||||
return;
|
||||
}
|
||||
if (data.error) {
|
||||
el.innerHTML = `<span style="color:var(--red)">${esc(data.error)}</span>`;
|
||||
return;
|
||||
}
|
||||
const verdictColor = data.verdict === "verlängert" ? "var(--green)" : data.verdict === "verkürzt" ? "var(--orange, #ffa657)" : "var(--text-dim)";
|
||||
el.innerHTML = `
|
||||
<div style="color:var(--text-dim)">Solarstrahlung nächste 3h: <strong style="color:var(--text)">${data.next_3h_max_ghi} W/m²</strong> · nächste 12h: <strong style="color:var(--text)">${data.horizon_12h_max_ghi} W/m²</strong></div>
|
||||
<div style="margin-top:4px;color:${verdictColor}">Beispiel bei ${data.example_base_h}h Basis-Timeout: <strong>${data.example_adjusted_h}h</strong> (${data.verdict})</div>`;
|
||||
}
|
||||
|
||||
async function savePrices() {
|
||||
const isSpot = document.getElementById("tariff-spot").checked;
|
||||
const body = {
|
||||
|
||||
Reference in New Issue
Block a user