From 3318d1344d4d2ffb5a5dd88a65fa7f7c0738c035 Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Wed, 26 Aug 2026 10:52:52 +0200 Subject: [PATCH] =?UTF-8?q?ShineBridge=20v1.11.0=20=E2=80=94=20Feature:=20?= =?UTF-8?q?automatisches=20t=C3=A4gliches=20Config-Backup=20nach=20/share?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Daemon-Thread (gleiches Muster wie der History-Cleanup-Thread) schreibt täglich einen Config-Export nach /share/shinebridge-backups/ — bewusst nicht nach /data, da genau dieser Ordner bei Add-on-Uninstall+Reinstall gelöscht wird (siehe Vorfall 2026-08-25). Export sofort beim Start plus alle 24h, 14 Tage Aufbewahrung mit automatischem Cleanup alter Dateien. Export-Datenaufbau aus /api/export-config in _build_export_data() extrahiert, von Endpoint und Scheduler gemeinsam genutzt. Neue Berechtigung map: share:rw in config.yaml. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- ROADMAP.md | 2 +- haos-addon/config.yaml | 4 +++- haos-addon/src/main.py | 46 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0c81d3b..fc2d1a5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Wechselrichter / Energiezähler - **Sparklines** — Live-Graphen der letzten 5 Minuten pro Sensor - **EMS-Controller** — PV-Überschussladen der Kathrein Wallbox, Zwangsladen-Fallback mit konfigurierbarem Max-Ladestrom - **Überschuss-Geräte** — Zigbee2MQTT-Geräte bei PV-Überschuss automatisch einschalten -- **Konfig-Export/Import** — JSON im Einstellungen-Tab +- **Konfig-Export/Import** — JSON im Einstellungen-Tab, zusätzlich automatisches tägliches Backup nach `/share/shinebridge-backups/` (übersteht auch Add-on-Uninstall+Reinstall, 14 Tage Aufbewahrung) - **Port-Sicherheit** — `/api/*` nur über HAOS-Ingress erreichbar, kein Direktzugriff von außen - **Kein Cloud-Zwang** — vollständig lokal diff --git a/ROADMAP.md b/ROADMAP.md index 3665dfa..c513583 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,7 +14,7 @@ Reihenfolge nach Aufwand/Abhängigkeit, nicht nach Wichtigkeit. -1. **Automatischer täglicher Config-Export** — nach dem Datenverlust-Vorfall vom 25.08. (Uninstall+Reinstall löschte `/data`) eine zusätzliche Absicherung neben den HAOS-eigenen Backups. Export-Endpunkt (`/api/export-config`) existiert schon, braucht nur einen Cron-Trigger + Ablagepfad. +1. ~~**Automatischer täglicher Config-Export**~~ — erledigt (v1.11.0): täglicher Export nach `/share/shinebridge-backups/`, 14 Tage Aufbewahrung, sofort beim Start plus alle 24h. 2. **Discord-Alerts** — bei Zwangsladen-Start, Fehlerzuständen (unbekannter Ladezustand), besonders günstigen Spot-Preisfenstern. Nutzt den bestehenden Homelab-Discord-Bot. 3. **Sessions pro Fahrzeug trennen** — falls mehrere Fahrer/Autos an einer Wallbox laden, aktuell nur eine Gesamt-Statistik. 4. **Lastspitzenkappung (Peak Shaving)** — Ladestrom drosseln, wenn Netzbezug (mehrere Autos + Haushalt gleichzeitig) einen Grenzwert überschreitet. Eigenes Sicherheits-Design nötig (was passiert bei Messfehler/Ausfall — nie ungebremst weiterladen). diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index 02a1080..521ccd4 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.10.3" +version: "1.11.0" slug: shinebridge description: Energiemanagement für Growatt/Goodwe-Wechselrichter — PV-Überschussladen, Wallbox-EMS, Tarifvergleich, Zigbee2MQTT-Überschussgeräte url: https://gitea.bitfire.work/retr0/shinebridge @@ -13,6 +13,8 @@ host_network: true ingress: true ingress_port: 8099 panel_icon: mdi:solar-power +map: + - share:rw options: mqtt_broker: "core-mosquitto" mqtt_port: 1883 diff --git a/haos-addon/src/main.py b/haos-addon/src/main.py index 67458e3..dc43322 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -808,10 +808,9 @@ def api_new_id(): return jsonify({"id": uuid.uuid4().hex[:8]}) -@app.get("/api/export-config") -def api_export_config(): +def _build_export_data() -> Dict[str, Any]: with State.lock: - data = { + return { "shinebridge_export": True, "version": 1, "exported_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), @@ -822,7 +821,47 @@ def api_export_config(): }, "inverters": State.inverters_cfg, } + + +CONFIG_BACKUP_DIR = "/share/shinebridge-backups" +CONFIG_BACKUP_RETENTION_DAYS = 14 + + +def _export_config_backup(): + """Schreibt einen taeglichen Config-Export nach /share — bewusst NICHT nach + /data, weil genau das der Ordner ist, der bei Add-on-Uninstall+Reinstall + geloescht wird (siehe Vorfall 2026-08-25). /share uebersteht das.""" + try: + os.makedirs(CONFIG_BACKUP_DIR, exist_ok=True) + filename = f"config-{time.strftime('%Y-%m-%d')}.json" + path = os.path.join(CONFIG_BACKUP_DIR, filename) + with open(path, "w", encoding="utf-8") as f: + json.dump(_build_export_data(), f, indent=2, ensure_ascii=False) + log.info("Config-Backup geschrieben: %s", path) + + cutoff = time.time() - CONFIG_BACKUP_RETENTION_DAYS * 86400 + for name in os.listdir(CONFIG_BACKUP_DIR): + fpath = os.path.join(CONFIG_BACKUP_DIR, name) + if name.startswith("config-") and name.endswith(".json") and os.path.getmtime(fpath) < cutoff: + os.remove(fpath) + log.info("Altes Config-Backup gelöscht: %s", fpath) + except OSError as e: + log.error("Config-Backup fehlgeschlagen: %s", e) + + +def _start_config_backup_scheduler(): + def _loop(): + _export_config_backup() # sofort beim Start, nicht erst nach 24h + while True: + time.sleep(86400) + _export_config_backup() + threading.Thread(target=_loop, daemon=True, name="config-backup").start() + + +@app.get("/api/export-config") +def api_export_config(): from flask import Response + data = _build_export_data() filename = f"shinebridge-config-{time.strftime('%Y%m%d-%H%M%S')}.json" return Response( json.dumps(data, indent=2, ensure_ascii=False), @@ -955,6 +994,7 @@ if __name__ == "__main__": save_config() _restart_all() + _start_config_backup_scheduler() port = int(os.environ.get("INGRESS_PORT", "8099")) log.info("Web UI startet auf Port %d", port) app.run(host="0.0.0.0", port=port, threaded=True)