Feature: Konfig-Export/Import im Web UI (Einstellungen-Tab)
- GET /api/export-config → JSON-Download mit allen Geräten + MQTT (ohne Passwort) - POST /api/import-config → Validierung + Übernahme + automatischer Neustart - Einstellungen-Tab: Exportieren-Button + Importieren-Dateiauswahl - btn-secondary CSS-Klasse hinzugefügt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -355,6 +355,59 @@ def api_get_models():
|
|||||||
def api_new_id():
|
def api_new_id():
|
||||||
return jsonify({"id": uuid.uuid4().hex[:8]})
|
return jsonify({"id": uuid.uuid4().hex[:8]})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/export-config")
|
||||||
|
def api_export_config():
|
||||||
|
with State.lock:
|
||||||
|
data = {
|
||||||
|
"shinebridge_export": True,
|
||||||
|
"version": 1,
|
||||||
|
"exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"mqtt": {
|
||||||
|
"broker": State.mqtt_cfg.get("mqtt_broker", ""),
|
||||||
|
"port": State.mqtt_cfg.get("mqtt_port", 1883),
|
||||||
|
"user": State.mqtt_cfg.get("mqtt_user", ""),
|
||||||
|
},
|
||||||
|
"inverters": State.inverters_cfg,
|
||||||
|
}
|
||||||
|
from flask import Response
|
||||||
|
filename = f"shinebridge-config-{time.strftime('%Y%m%d-%H%M%S')}.json"
|
||||||
|
return Response(
|
||||||
|
json.dumps(data, indent=2, ensure_ascii=False),
|
||||||
|
mimetype="application/json",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/import-config")
|
||||||
|
def api_import_config():
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
if not data.get("shinebridge_export"):
|
||||||
|
return jsonify({"error": "Keine gültige ShineBridge-Export-Datei"}), 400
|
||||||
|
|
||||||
|
inverters = data.get("inverters", [])
|
||||||
|
if not isinstance(inverters, list):
|
||||||
|
return jsonify({"error": "inverters ungültig"}), 400
|
||||||
|
for inv in inverters:
|
||||||
|
if not inv.get("modbus_ip"):
|
||||||
|
return jsonify({"error": f"modbus_ip fehlt in Gerät {inv.get('name', '?')}"}), 400
|
||||||
|
if inv.get("inverter_model") not in INVERTERS:
|
||||||
|
return jsonify({"error": f"Unbekanntes Modell: {inv.get('inverter_model')}"}), 400
|
||||||
|
|
||||||
|
with State.lock:
|
||||||
|
if mqtt := data.get("mqtt"):
|
||||||
|
if mqtt.get("broker"):
|
||||||
|
State.mqtt_cfg["mqtt_broker"] = mqtt["broker"]
|
||||||
|
if mqtt.get("port"):
|
||||||
|
State.mqtt_cfg["mqtt_port"] = int(mqtt["port"])
|
||||||
|
if mqtt.get("user"):
|
||||||
|
State.mqtt_cfg["mqtt_user"] = mqtt["user"]
|
||||||
|
State.inverters_cfg = inverters
|
||||||
|
save_config()
|
||||||
|
|
||||||
|
threading.Thread(target=_restart_all, daemon=True).start()
|
||||||
|
return jsonify({"ok": True, "inverters": len(inverters)})
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def index():
|
def index():
|
||||||
return send_from_directory(WEB_DIR, "index.html")
|
return send_from_directory(WEB_DIR, "index.html")
|
||||||
|
|||||||
@@ -91,6 +91,8 @@
|
|||||||
.btn-danger:hover { background: rgba(248,81,73,.1); }
|
.btn-danger:hover { background: rgba(248,81,73,.1); }
|
||||||
.btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 700; }
|
.btn-primary { background: var(--accent); color: #000; border-color: var(--accent); font-weight: 700; }
|
||||||
.btn-primary:hover { opacity: .85; }
|
.btn-primary:hover { opacity: .85; }
|
||||||
|
.btn-secondary { color: var(--blue); border-color: var(--blue); }
|
||||||
|
.btn-secondary:hover { background: rgba(88,166,255,.1); }
|
||||||
.inv-card-meta { font-size: 12px; color: var(--text-dim); line-height: 1.7; }
|
.inv-card-meta { font-size: 12px; color: var(--text-dim); line-height: 1.7; }
|
||||||
.add-btn { display: flex; align-items: center; justify-content: center; gap: 8px;
|
.add-btn { display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||||
padding: 20px; background: var(--surface); border: 2px dashed var(--border);
|
padding: 20px; background: var(--surface); border: 2px dashed var(--border);
|
||||||
@@ -189,6 +191,19 @@
|
|||||||
<input type="password" id="cfg-mqtt-pass" placeholder="leer = unverändert"></div>
|
<input type="password" id="cfg-mqtt-pass" placeholder="leer = unverändert"></div>
|
||||||
<button class="btn btn-primary" onclick="saveMqtt()">Speichern & Neu starten</button>
|
<button class="btn btn-primary" onclick="saveMqtt()">Speichern & Neu starten</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Konfiguration sichern</h3>
|
||||||
|
<p style="color:var(--text-dim);font-size:.85rem;margin:0 0 .75rem">Alle Geräte und MQTT-Einstellungen als JSON exportieren und bei einer Neuinstallation wieder einlesen.</p>
|
||||||
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-secondary" onclick="exportConfig()">⇓ Exportieren</button>
|
||||||
|
<label class="btn btn-secondary" style="cursor:pointer">
|
||||||
|
⇑ Importieren
|
||||||
|
<input type="file" id="import-file-input" accept=".json" style="display:none" onchange="importConfig(this)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="import-result" style="margin-top:.5rem;font-size:.85rem"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -529,6 +544,37 @@ async function saveMqtt() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Export / Import ───────────────────────────────────────────
|
||||||
|
|
||||||
|
function exportConfig() {
|
||||||
|
window.location.href = api("api/export-config");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importConfig(input) {
|
||||||
|
const file = input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const resultEl = document.getElementById("import-result");
|
||||||
|
resultEl.textContent = "Wird geladen…";
|
||||||
|
resultEl.style.color = "var(--text-dim)";
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
const res = await fetchJSON(api("api/import-config"), {
|
||||||
|
method: "POST", headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
resultEl.textContent = `✓ ${res.inverters} Gerät(e) importiert. Neustart läuft…`;
|
||||||
|
resultEl.style.color = "var(--green)";
|
||||||
|
showToast(`${res.inverters} Gerät(e) importiert`, "ok");
|
||||||
|
setTimeout(() => { loadSettings(); loadInverters(); }, 3000);
|
||||||
|
} catch(e) {
|
||||||
|
resultEl.textContent = `Fehler: ${e.message}`;
|
||||||
|
resultEl.style.color = "var(--red)";
|
||||||
|
showToast("Import fehlgeschlagen", "err");
|
||||||
|
}
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user