diff --git a/.gitignore b/.gitignore index 4a684c8..0910caf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__/ # versehentlich getrackte Alt-Dateien, gehoeren nicht ins Repo Modbus-Server-Kathrein.pdf ShineLAN-X/releases/*.bin +growatt-diagnose-tool/ diff --git a/haos-addon/.gitignore b/haos-addon/.gitignore new file mode 100644 index 0000000..ed74fc1 --- /dev/null +++ b/haos-addon/.gitignore @@ -0,0 +1 @@ +firmware/ diff --git a/haos-addon/Dockerfile b/haos-addon/Dockerfile index 151bcc7..2286802 100644 --- a/haos-addon/Dockerfile +++ b/haos-addon/Dockerfile @@ -4,7 +4,6 @@ FROM ${BUILD_FROM} WORKDIR /app COPY src/ /app/ -COPY firmware/ /firmware/ RUN pip3 install --no-cache-dir \ pymodbus==3.6.9 \ diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index eae45b0..bb633fe 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,7 +1,7 @@ name: ShineBridge -version: "1.8.31" +version: "1.9.0" slug: shinebridge -description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI +description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte url: https://gitea.bitfire.work/retr0/shinebridge arch: - aarch64 diff --git a/haos-addon/firmware/nuttx-mbusd-shinelanx-dfu.bin b/haos-addon/firmware/nuttx-mbusd-shinelanx-dfu.bin deleted file mode 100755 index 1308bdf..0000000 Binary files a/haos-addon/firmware/nuttx-mbusd-shinelanx-dfu.bin and /dev/null differ diff --git a/haos-addon/firmware/nuttx-mbusd-shinelanx.bin b/haos-addon/firmware/nuttx-mbusd-shinelanx.bin deleted file mode 100755 index 8e69b46..0000000 Binary files a/haos-addon/firmware/nuttx-mbusd-shinelanx.bin and /dev/null differ diff --git a/haos-addon/src/main.py b/haos-addon/src/main.py index 0138d18..e90f95f 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -1,7 +1,6 @@ import json import logging import os -import re import threading import time import uuid @@ -900,67 +899,8 @@ def api_spot_price(): log.warning("Spot-Price API Fehler: %s", e) return jsonify({"ok": False, "data": _spot_cache.get("data", [])}) -_IP_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$') - -@app.get("/api/flash/probe") -def api_flash_probe(): - import urllib.request as _ur - ip = request.args.get("ip", "").strip() - if not ip or not _IP_RE.match(ip): - return jsonify({"ota": False, "error": "invalid ip"}), 400 - try: - with _ur.urlopen(f"http://{ip}/", timeout=5) as r: - data = json.loads(r.read().decode()) - return jsonify({"ota": bool(data.get("ota")), "info": data}) - except Exception: - return jsonify({"ota": False}) - -@app.get("/api/flash/firmware") -def api_flash_firmware(): - fw_dir = "/firmware" - try: - files = sorted(f for f in os.listdir(fw_dir) if f.endswith(".bin")) - return jsonify({"files": files}) - except Exception: - return jsonify({"files": []}) - -@app.post("/api/flash/update") -def api_flash_update(): - import urllib.request as _ur - ip = request.args.get("ip", "").strip() - fw_name = request.args.get("fw", "").strip() - if not ip or not _IP_RE.match(ip): - return jsonify({"ok": False, "error": "invalid ip"}), 400 - try: - if fw_name: - fw_path = os.path.join("/firmware", os.path.basename(fw_name)) - with open(fw_path, "rb") as f: - data = f.read() - else: - data = request.get_data() - if len(data) < 256: - return jsonify({"ok": False, "error": "firmware too small"}), 400 - req = _ur.Request( - f"http://{ip}/update", data=data, method="POST", - headers={"Content-Type": "application/octet-stream", - "Content-Length": str(len(data))}) - with _ur.urlopen(req, timeout=90) as r: - return jsonify({"ok": True, "response": r.read().decode(errors="replace")}) - except Exception as e: - return jsonify({"ok": False, "error": str(e)}), 502 - -@app.post("/api/flash/reboot") -def api_flash_reboot(): - import urllib.request as _ur - ip = request.args.get("ip", "").strip() - if not ip or not _IP_RE.match(ip): - return jsonify({"ok": False, "error": "invalid ip"}), 400 - try: - req = _ur.Request(f"http://{ip}/reboot", data=b"", method="POST") - with _ur.urlopen(req, timeout=10) as r: - return jsonify({"ok": True}) - except Exception as e: - return jsonify({"ok": False, "error": str(e)}), 502 +# Flash/OTA-Endpunkte ausgelagert nach growatt-diagnose-tool (v1.9.0) — +# ShineBridge fokussiert sich auf Energiemanagement, kein Firmware-Flashing mehr. @app.get("/") def index(): diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index e7fa7fd..32c05db 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -162,48 +162,14 @@ .toast.ok { border-color: var(--green); color: var(--green); } .toast.err { border-color: var(--red); color: var(--red); } - /* Setup Wizard */ - .wz-overlay { position:fixed; inset:0; background:rgba(0,0,0,.88); z-index:300; - display:flex; align-items:center; justify-content:center; padding:20px; } - .wz-box { background:var(--surface); border:1px solid var(--border); - border-radius:14px; padding:32px; width:100%; max-width:460px; } - .wz-logo { text-align:center; margin-bottom:22px; } - .wz-logo h2 { font-size:19px; margin-bottom:4px; } - .wz-logo p { color:var(--text-dim); font-size:13px; } - .wz-stepper { display:flex; margin-bottom:28px; } - .wz-step { flex:1; text-align:center; font-size:11px; font-weight:600; - text-transform:uppercase; letter-spacing:.05em; padding:7px 4px; - color:var(--text-dim); border-bottom:2px solid var(--border); - transition:color .2s, border-color .2s; } - .wz-step.active { color:var(--accent); border-color:var(--accent); } - .wz-step.done { color:var(--green); border-color:var(--green); } - .wz-panel { display:none; } - .wz-panel.active { display:block; } - .wz-actions { display:flex; justify-content:space-between; align-items:center; margin-top:22px; } - .wz-skip { font-size:12px; color:var(--text-dim); cursor:pointer; - background:none; border:none; color:var(--text-dim); padding:0; } - .wz-skip:hover { color:var(--text); } - @media (max-width: 520px) { main { padding: 10px; } .settings-section { max-width: 100%; } .sensor-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } .inv-list { grid-template-columns: 1fr; } .kwh-card .kv { font-size: 15px; } - .flash-section { max-width: 100% !important; } header svg { display: none; } } - - /* Flash-Wizard */ - .flash-section { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; max-width:560px; } - .flash-section h3 { font-size:13px; font-weight:600; color:var(--text-dim); text-transform:uppercase; letter-spacing:.06em; margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid var(--border); } - .flash-code { font-family:monospace; font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; padding:10px 14px; margin:6px 0; word-break:break-all; } - .flash-step { display:flex; gap:10px; margin-bottom:14px; align-items:flex-start; } - .flash-step-num { width:22px; height:22px; min-width:22px; border-radius:50%; background:var(--accent); color:#000; font-size:12px; font-weight:700; display:flex; align-items:center; justify-content:center; } - .flash-pinout { font-family:monospace; font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:6px; padding:10px 14px; line-height:1.9; white-space:pre; } - .flash-progress { width:100%; height:6px; background:var(--surface2); border-radius:3px; overflow:hidden; margin:12px 0 6px; } - .flash-progress-bar { height:100%; background:var(--accent); border-radius:3px; transition:width .4s; } - .flash-status-msg { font-size:12px; color:var(--text-dim); min-height:18px; } @@ -229,7 +195,6 @@
Live-Daten
Geräte
Einstellungen
-
Flash
@@ -358,99 +323,7 @@ - -
- - -
-

ShineLAN-X Stick

-
- - -
-
IP eingeben und auf Verbinden klicken.
-
- - - - - - - -
+ @@ -507,77 +380,6 @@ GND → GND - - -
@@ -656,7 +458,7 @@ function showToast(msg, type) { } function switchTab(name) { - ["energy","finance","live","inverters","settings","flash"].forEach((t, i) => { + ["energy","finance","live","inverters","settings"].forEach((t, i) => { document.querySelectorAll(".tab")[i].classList.toggle("active", t === name); document.querySelectorAll(".panel")[i].classList.toggle("active", t === name); }); @@ -1534,214 +1336,13 @@ function renderSurplusStatus(surplusData) { `; } -// ── Flash-Wizard ────────────────────────────────────────────── - -function flashFwSourceChange() { - const bundled = document.getElementById("flash-fw-bundled").checked; - document.getElementById("flash-fw-bundled-ui").style.display = bundled ? "" : "none"; - document.getElementById("flash-fw-custom-ui").style.display = bundled ? "none" : ""; -} - -async function flashProbe() { - const ip = document.getElementById("flash-ip").value.trim(); - const statusEl = document.getElementById("flash-probe-status"); - const otaSection = document.getElementById("flash-ota-section"); - if (!ip) { showToast("IP-Adresse eingeben", "err"); return; } - statusEl.textContent = "Verbinde..."; - statusEl.style.color = "var(--text-dim)"; - try { - const r = await fetchJSON(api(`api/flash/probe?ip=${encodeURIComponent(ip)}`)); - if (r.ota) { - statusEl.textContent = "✓ OTA-Modus aktiv"; - statusEl.style.color = "var(--green)"; - otaSection.style.display = ""; - await flashLoadFirmwareList(); - } else { - statusEl.textContent = "✗ Nicht erreichbar — ST-Link-Erstflash erforderlich"; - statusEl.style.color = "var(--red)"; - otaSection.style.display = "none"; - } - } catch(e) { - statusEl.textContent = "Verbindungsfehler"; - statusEl.style.color = "var(--red)"; - otaSection.style.display = "none"; - } -} - -async function flashLoadFirmwareList() { - try { - const r = await fetchJSON(api("api/flash/firmware")); - const sel = document.getElementById("flash-fw-select"); - sel.innerHTML = ""; - if (r.files && r.files.length) { - r.files.forEach(f => { - const opt = document.createElement("option"); - opt.value = f; opt.textContent = f; - sel.appendChild(opt); - }); - const dfu = r.files.find(f => f.includes("dfu")); - if (dfu) sel.value = dfu; - } else { - sel.innerHTML = ''; - document.getElementById("flash-fw-custom").checked = true; - flashFwSourceChange(); - } - } catch(e) { /* ignore */ } -} - -async function flashStart() { - const ip = document.getElementById("flash-ip").value.trim(); - if (!ip) { showToast("IP-Adresse eingeben", "err"); return; } - const bundled = document.getElementById("flash-fw-bundled").checked; - const progressWrap = document.getElementById("flash-progress-wrap"); - const progressBar = document.getElementById("flash-progress-bar"); - const statusMsg = document.getElementById("flash-status-msg"); - const startBtn = document.getElementById("flash-start-btn"); - - progressWrap.style.display = ""; - progressBar.style.background = "var(--accent)"; - progressBar.style.width = "0%"; - startBtn.disabled = true; - - let url, opts; - if (bundled) { - const fw = document.getElementById("flash-fw-select").value; - if (!fw) { showToast("Keine Firmware ausgewählt", "err"); startBtn.disabled = false; return; } - url = api(`api/flash/update?ip=${encodeURIComponent(ip)}&fw=${encodeURIComponent(fw)}`); - opts = { method: "POST" }; - statusMsg.textContent = `Übertrage ${fw} …`; - } else { - const file = document.getElementById("flash-fw-file").files[0]; - if (!file) { showToast("Datei auswählen", "err"); startBtn.disabled = false; return; } - url = api(`api/flash/update?ip=${encodeURIComponent(ip)}`); - opts = { method: "POST", body: await file.arrayBuffer(), - headers: {"Content-Type": "application/octet-stream"} }; - statusMsg.textContent = `Übertrage ${file.name} …`; - } - - progressBar.style.width = "25%"; - try { - const resp = await fetch(url, opts); - progressBar.style.width = "70%"; - const result = await resp.json(); - if (result.ok) { - statusMsg.textContent = "Warte auf Neustart …"; - progressBar.style.width = "85%"; - await flashPollReady(ip); - progressBar.style.width = "100%"; - progressBar.style.background = "var(--green)"; - statusMsg.textContent = "✓ Update abgeschlossen — Stick ist wieder online."; - showToast("Firmware erfolgreich geflasht", "ok"); - } else { - progressBar.style.width = "100%"; - progressBar.style.background = "var(--red)"; - statusMsg.textContent = `Fehler: ${result.error || "Unbekannt"}`; - showToast("Flash fehlgeschlagen", "err"); - } - } catch(e) { - progressBar.style.background = "var(--red)"; - statusMsg.textContent = `Verbindungsfehler: ${e.message}`; - showToast("Flash fehlgeschlagen", "err"); - } - startBtn.disabled = false; -} - -async function flashPollReady(ip, maxSec = 90) { - const start = Date.now(); - while ((Date.now() - start) / 1000 < maxSec) { - await new Promise(r => setTimeout(r, 3000)); - try { - const r = await fetchJSON(api(`api/flash/probe?ip=${encodeURIComponent(ip)}`)); - if (r.ota) return; - } catch(e) { /* still booting */ } - } -} - -async function flashReboot() { - const ip = document.getElementById("flash-ip").value.trim(); - if (!ip) { showToast("IP-Adresse eingeben", "err"); return; } - try { - await fetch(api(`api/flash/reboot?ip=${encodeURIComponent(ip)}`), { method: "POST" }); - showToast("Neustart-Befehl gesendet", "ok"); - } catch(e) { showToast("Neustart fehlgeschlagen", "err"); } -} - -// ── Setup Wizard ────────────────────────────────────────────── - -function wizardGoStep(n) { - for (let i = 1; i <= 3; i++) { - document.getElementById(`wz-panel-${i}`).classList.toggle("active", i === n); - const el = document.getElementById(`wz-step-${i}`); - el.classList.toggle("active", i === n); - el.classList.toggle("done", i < n); - } -} - -async function wizardStep1Next() { - const body = { - mqtt_broker: document.getElementById("wz-mqtt-broker").value.trim() || "core-mosquitto", - mqtt_port: parseInt(document.getElementById("wz-mqtt-port").value) || 1883, - mqtt_user: document.getElementById("wz-mqtt-user").value, - mqtt_pass: document.getElementById("wz-mqtt-pass").value, - }; - try { - await fetchJSON(api("api/config"), { - method: "POST", headers: {"Content-Type": "application/json"}, - body: JSON.stringify(body), - }); - } catch(e) { showToast("Speichern fehlgeschlagen", "err"); return; } - wizardGoStep(2); -} - -async function wizardStep2Next() { - const name = document.getElementById("wz-inv-name").value.trim(); - const ip = document.getElementById("wz-inv-ip").value.trim(); - if (!name || !ip) { showToast("Name und IP sind Pflichtfelder", "err"); return; } - const model = document.getElementById("wz-inv-model").value; - const id = Math.random().toString(36).slice(2, 10); - const newInv = { - id, - name, - inverter_model: model, - modbus_ip: ip, - modbus_port: parseInt(document.getElementById("wz-inv-port").value) || 502, - modbus_address: parseInt(document.getElementById("wz-inv-addr").value) || 1, - mqtt_topic_prefix: `growatt/${name.toLowerCase().replace(/\s+/g, "_")}`, - update_interval: 30, - }; - // Bestehende Geräte behalten — Wizard darf nicht die komplette Liste überschreiben - const body = [...invertersList, newInv]; - try { - await fetchJSON(api("api/inverters-config"), { - method: "POST", headers: {"Content-Type": "application/json"}, - body: JSON.stringify(body), - }); - } catch(e) { showToast("Speichern fehlgeschlagen", "err"); return; } - wizardGoStep(3); -} - -function wizardFinish() { - document.getElementById("wizard-overlay").style.display = "none"; - loadSettings(); - loadInverters(); -} - -function wizardSkip() { - document.getElementById("wizard-overlay").style.display = "none"; -} +// Flash-Wizard + Setup-Wizard ausgelagert nach growatt-diagnose-tool (v1.9.0) — +// Geräte werden jetzt direkt über "+ Gerät hinzufügen" im Geräte-Tab angelegt. // ── Init ────────────────────────────────────────────────────── (async () => { await Promise.all([loadSettings(), loadInverters(), loadModels()]); - if (!globalConfig.inverters || globalConfig.inverters.length === 0) { - document.getElementById("wz-mqtt-broker").value = globalConfig.mqtt_broker || "core-mosquitto"; - document.getElementById("wz-mqtt-port").value = globalConfig.mqtt_port || 1883; - document.getElementById("wz-mqtt-user").value = globalConfig.mqtt_user || ""; - document.getElementById("wz-inv-model").innerHTML = - document.getElementById("modal-model").innerHTML; - document.getElementById("wizard-overlay").style.display = "flex"; - } startRefresh(); })();