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
-
-
- Verbinden
-
-
IP eingeben und auf Verbinden klicken.
-
-
-
-
-
OTA-Update
-
-
-
- Firmware flashen
- Neu starten
-
-
-
-
-
-
Erstflash via ST-Link
-
Wenn der Stick noch nie geflasht wurde, ist OTA noch nicht verfügbar. Einen ST-Link v2 und stlink-tools werden benötigt.
-
-
-
1
-
-
SWD-Verbindung herstellen
-
PA13 → SWDIO
-PA14 → SWCLK
-GND → GND
-3.3V → 3.3V (falls nicht extern versorgt)
-
-
-
-
-
2
-
-
Flash-Kommando ausführen
-
st-flash --reset write nuttx-mbusd-shinelanx.bin 0x08000000
-
Datei im ShineLAN-X Releases-Ordner des Repos oder als GitHub-Release.
-
-
-
-
-
3
-
-
Auf DHCP-Adresse warten
-
Nach dem Neustart erscheint der Stick im Netz. IP oben eintragen → Verbinden — danach sind OTA-Updates möglich.
-
-
-
-
- Nach dem Erstflash empfiehlt sich ein OTA-Update auf die DFU-Variante
- (nuttx-mbusd-shinelanx-dfu.bin),
- die zukünftige OTA-Updates ohne ST-Link ermöglicht.
-
-
-
-
+
@@ -507,77 +380,6 @@ GND → GND
-
-
-
-
-
-
-
-
-
Willkommen bei ShineBridge
-
Schnelle Einrichtung — 3 Schritte
-
-
-
1 · MQTT
-
2 · Wechselrichter
-
3 · Fertig
-
-
-
-
-
-
-
-
Name
-
-
Gerätetyp
-
-
IP-Adresse (ShineLAN-X)
-
-
-
- ← Zurück
- Weiter →
-
-
-
-
-
-
-
✓
-
Einrichtung abgeschlossen!
-
- ShineBridge verbindet sich jetzt mit MQTT und deinem Wechselrichter.
- Die ersten Messwerte erscheinen nach wenigen Sekunden.
-
-
-
- Los geht's →
-
-
-
-
-
@@ -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 = 'Keine integrierten Firmwares ';
- 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();
})();