From 6f30fe6b1caea00122e31c9c37c8ccfbca01f63d Mon Sep 17 00:00:00 2001 From: retr0 <42kdesigners@gmail.com> Date: Tue, 25 Aug 2026 14:28:45 +0200 Subject: [PATCH] =?UTF-8?q?ShineBridge=20v1.10.0=20=E2=80=94=20Neuer=20Ger?= =?UTF-8?q?=C3=A4te-Typ:=20Growatt=20ShineWifi-X=20=C3=BCber=20bestehende?= =?UTF-8?q?=20HA-Entity=20(ESPHome)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Statt selbst per Modbus/UDP zu pollen, kann ShineBridge jetzt einen Wechselrichter einbinden, der bereits per ESPHome nativ in Home Assistant läuft — nützlich für die ShineWifi-X-Variante, die keine eigene Modbus-TCP-Schnittstelle hat. - Neuer ha_entity_client.py: liest Sensor-Werte über die interne Supervisor-API (http://supervisor/core/api), braucht dafür 'homeassistant_api: true' in config.yaml (neu gesetzt — Add-on muss dafür einmal neu installiert werden) - Neuer Geräte-Typ 'GROWATT_WIFI_X' in inverters.py, protocol='ha_entity' - Feste Suffix-Zuordnung (dcpower→pv_power, acpower→ac_power, energytoday→ energy_today, ...) passend zur esphome-growatt-Komponente aus growatt-diagnose-tool/ShineWifi-X — Entity-Id ergibt sich als sensor._ - Neues Feld 'HA Entity-Prefix' im Geräte-Dialog statt Modbus-IP/Port/Adresse, blendet sich abhängig vom gewählten Gerätetyp ein/aus - Läuft danach durch dieselbe Aggregations-/Dashboard-Logik wie jedes andere Gerät (kein Sonderfall in EMS/Finanzen nötig, da Netzbezug bereits über den bestehenden Zähler erfasst wird — dieser Wechselrichter liefert nur PV-Werte) Co-Authored-By: Claude Sonnet 5 --- haos-addon/config.yaml | 3 +- haos-addon/src/ha_entity_client.py | 73 ++++++++++++++++++++++++++++++ haos-addon/src/inverters.py | 26 +++++++++++ haos-addon/src/main.py | 7 ++- haos-addon/src/web/index.html | 37 +++++++++++---- 5 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 haos-addon/src/ha_entity_client.py diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index ee223de..83bd29f 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.9.6" +version: "1.10.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,7 @@ host_network: true ingress: true ingress_port: 8099 panel_icon: mdi:solar-power +homeassistant_api: true options: mqtt_broker: "core-mosquitto" mqtt_port: 1883 diff --git a/haos-addon/src/ha_entity_client.py b/haos-addon/src/ha_entity_client.py new file mode 100644 index 0000000..4a89802 --- /dev/null +++ b/haos-addon/src/ha_entity_client.py @@ -0,0 +1,73 @@ +import json +import logging +import os +import urllib.error +import urllib.request +from typing import Dict, Optional + +log = logging.getLogger(__name__) + +# Suffix am ESPHome-Entity-Namen -> ShineBridge-interner Sensor-Id. +# Passt zur esphome-growatt-Komponente (siehe growatt-diagnose-tool/ShineWifi-X) — +# Entity-Id ergibt sich als sensor._, z.B. sensor.growatt_wifi_x_1_dcpower. +ENTITY_SUFFIX_MAP = { + "pv_voltage": "dcvoltage", + "pv_current": "dcinputcurrent", + "pv_power": "dcpower", + "grid_frequency": "acfrequency", + "grid_voltage": "acvoltage", + "grid_current": "acoutputcurrent", + "ac_power": "acpower", + "energy_today": "energytoday", + "energy_total": "energytotal", + "inverter_temp": "temperature", +} + + +class HAEntityReader: + """Liest Wechselrichter-Werte aus bereits bestehenden Home-Assistant-Entities + (z.B. ein per ESPHome direkt in HA eingebundener ShineWifi-X-Stick) statt sie + selbst per Modbus/UDP zu pollen. + + Braucht 'homeassistant_api: true' in config.yaml — HAOS injiziert dann + SUPERVISOR_TOKEN und die interne API ist unter http://supervisor/core/api + erreichbar (kein manuell erstelltes Long-Lived Token nötig).""" + + BASE_URL = "http://supervisor/core/api" + + def __init__(self, entity_prefix: str): + self.entity_prefix = entity_prefix + self.token = os.environ.get("SUPERVISOR_TOKEN", "") + + def read(self, inverter=None) -> Optional[Dict[str, float]]: + if not self.entity_prefix: + log.error("[HA-Entity] Kein Entity-Prefix konfiguriert") + return None + if not self.token: + log.error("[HA-Entity %s] Kein SUPERVISOR_TOKEN — 'homeassistant_api: true' in config.yaml gesetzt und Add-on neu installiert?", self.entity_prefix) + return None + + result: Dict[str, float] = {} + any_ok = False + for sensor_id, suffix in ENTITY_SUFFIX_MAP.items(): + val = self._get_state(f"sensor.{self.entity_prefix}_{suffix}") + if val is not None: + result[sensor_id] = val + any_ok = True + return result if any_ok else None + + def _get_state(self, entity_id: str) -> Optional[float]: + req = urllib.request.Request( + f"{self.BASE_URL}/states/{entity_id}", + headers={"Authorization": f"Bearer {self.token}"}, + ) + try: + with urllib.request.urlopen(req, timeout=5) as r: + data = json.loads(r.read()) + return round(float(data["state"]), 3) + except (urllib.error.HTTPError, urllib.error.URLError, ValueError, KeyError) as e: + log.debug("[HA-Entity] %s nicht lesbar: %s", entity_id, e) + return None + + def close(self): + pass diff --git a/haos-addon/src/inverters.py b/haos-addon/src/inverters.py index 61e649b..b0ba3b1 100644 --- a/haos-addon/src/inverters.py +++ b/haos-addon/src/inverters.py @@ -197,6 +197,23 @@ def _kathrein_sensors() -> List[Sensor]: ] +def _wifi_x_sensors() -> List[Sensor]: + # reg/count/scale/data_type ungenutzt bei protocol="ha_entity" — Werte kommen + # fertig geparst von HAEntityReader, hier nur Metadaten fuer MQTT-Discovery. + return [ + Sensor("pv_voltage", "PV Spannung", 0, 1, 1.0, "V", "voltage", "measurement", "mdi:solar-panel"), + Sensor("pv_current", "PV Strom", 0, 1, 1.0, "A", "current", "measurement", "mdi:solar-panel"), + Sensor("pv_power", "PV Leistung", 0, 1, 1.0, "W", "power", "measurement", "mdi:solar-panel"), + Sensor("grid_frequency", "Netz-Frequenz", 0, 1, 1.0, "Hz", "frequency", "measurement", "mdi:sine-wave"), + Sensor("grid_voltage", "Netz-Spannung", 0, 1, 1.0, "V", "voltage", "measurement", "mdi:flash"), + Sensor("grid_current", "Netz-Strom", 0, 1, 1.0, "A", "current", "measurement", "mdi:flash"), + Sensor("ac_power", "AC Ausgangsleistung", 0, 1, 1.0, "W", "power", "measurement", "mdi:flash"), + Sensor("energy_today", "Energie Heute", 0, 1, 1.0, "kWh", "energy", "total_increasing", "mdi:solar-power"), + Sensor("energy_total", "Energie Gesamt", 0, 1, 1.0, "kWh", "energy", "total_increasing", "mdi:solar-power"), + Sensor("inverter_temp", "Wechselrichter Temp.", 0, 1, 1.0, "°C", "temperature", "measurement", "mdi:thermometer"), + ] + + INVERTERS = { "MIC_1500_TL_X": Inverter( id="MIC_1500_TL_X", @@ -253,4 +270,13 @@ INVERTERS = { protocol="kathrein", goodwe_family="", ), + "GROWATT_WIFI_X": Inverter( + id="GROWATT_WIFI_X", + name="Growatt ShineWifi-X (ESPHome)", + manufacturer="Growatt", + sensors=_wifi_x_sensors(), + read_ranges=[], + protocol="ha_entity", + goodwe_family="", + ), } diff --git a/haos-addon/src/main.py b/haos-addon/src/main.py index e90f95f..998642c 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -14,6 +14,7 @@ import history from modbus_client import ModbusReader from goodwe_client import GoodweReader from wallbox_client import WallboxReader +from ha_entity_client import HAEntityReader from ems_controller import EmsController from mqtt_publisher import MqttPublisher from surplus_devices import SurplusDeviceController @@ -272,12 +273,16 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event): log.error("[%s] Ungültige Konfiguration: %s", inv_id, e) return - host = inv_cfg["modbus_ip"] + host = inv_cfg.get("modbus_ip", "") ems: Optional[EmsController] = None if inverter.protocol == "goodwe_udp": reader = GoodweReader(host=host, family=inverter.goodwe_family) log.info("[%s] Poll-Loop: %s @ %s (Goodwe UDP/8899) alle %ds", inv_id, inverter.name, host, interval) + elif inverter.protocol == "ha_entity": + reader = HAEntityReader(entity_prefix=inv_cfg.get("ha_entity_prefix", "")) + log.info("[%s] Poll-Loop: %s (HA-Entity Prefix: %s) alle %ds", + inv_id, inverter.name, inv_cfg.get("ha_entity_prefix", ""), interval) elif inverter.protocol == "kathrein": reader = WallboxReader(host=host, port=port, timeout=min(5.0, interval * 0.4)) ems = EmsController( diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index c540942..2dce7d0 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -335,12 +335,19 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -1036,7 +1043,9 @@ function renderInverterList() {
- 📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}
+ ${inv.inverter_model === "GROWATT_WIFI_X" + ? `🏠 HA-Entity: ${esc(inv.ha_entity_prefix || "–")}` + : `📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}`}
📨 ${esc(inv.mqtt_topic_prefix)}
⏱ alle ${inv.update_interval || 30}s
@@ -1049,16 +1058,19 @@ function renderInverterList() { el.innerHTML = cards + `
+ Gerät hinzufügen
`; } -function toggleEmsSection(model) { +function onModelChange(model) { document.getElementById("ems-section").style.display = model === "KATHREIN_WALLBOX" ? "" : "none"; + const isHaEntity = model === "GROWATT_WIFI_X"; + document.getElementById("modbus-fields").style.display = isHaEntity ? "none" : ""; + document.getElementById("ha-entity-fields").style.display = isHaEntity ? "" : "none"; } async function openModal(invId) { await loadModels(); const modal = document.getElementById("modal-backdrop"); const modelSel = document.getElementById("modal-model"); - modelSel.onchange = () => toggleEmsSection(modelSel.value); + modelSel.onchange = () => onModelChange(modelSel.value); if (invId) { const inv = invertersList.find(i => i.id === invId) || {}; document.getElementById("modal-title").textContent = "Gerät bearbeiten"; @@ -1068,6 +1080,7 @@ async function openModal(invId) { document.getElementById("modal-ip").value = inv.modbus_ip || ""; document.getElementById("modal-port").value = inv.modbus_port || 502; document.getElementById("modal-addr").value = inv.modbus_address || 1; + document.getElementById("modal-ha-prefix").value = inv.ha_entity_prefix || ""; document.getElementById("modal-prefix").value = inv.mqtt_topic_prefix || ""; document.getElementById("modal-interval").value = inv.update_interval || 30; document.getElementById("ems-min-pv").value = inv.ems_min_pv ?? 1400; @@ -1083,6 +1096,7 @@ async function openModal(invId) { document.getElementById("modal-ip").value = ""; document.getElementById("modal-port").value = "502"; document.getElementById("modal-addr").value = "1"; + document.getElementById("modal-ha-prefix").value = ""; document.getElementById("modal-prefix").value = "growatt/wechselrichter" + (invertersList.length + 1); document.getElementById("modal-interval").value = "30"; document.getElementById("ems-min-pv").value = "1400"; @@ -1091,7 +1105,7 @@ async function openModal(invId) { document.getElementById("ems-phases").value = "3"; document.getElementById("ems-max-current").value = "32"; } - toggleEmsSection(modelSel.value); + onModelChange(modelSel.value); modal.classList.add("open"); } @@ -1119,6 +1133,9 @@ async function saveInverter() { mqtt_topic_prefix: document.getElementById("modal-prefix").value.trim(), update_interval: parseInt(document.getElementById("modal-interval").value), }; + if (model === "GROWATT_WIFI_X") { + inv.ha_entity_prefix = document.getElementById("modal-ha-prefix").value.trim(); + } if (model === "KATHREIN_WALLBOX") { inv.ems_min_pv = parseInt(document.getElementById("ems-min-pv").value); inv.ems_timeout = parseFloat(document.getElementById("ems-timeout").value);