ShineBridge v1.10.0 — Neuer Geräte-Typ: Growatt ShineWifi-X über bestehende HA-Entity (ESPHome)
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.<prefix>_<suffix> - 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.<prefix>_<suffix>, 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
|
||||
@@ -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="",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -335,12 +335,19 @@
|
||||
<input type="text" id="modal-name" placeholder="z.B. Dach Süd"></div>
|
||||
<div class="field"><label>Gerätetyp</label>
|
||||
<select id="modal-model"></select></div>
|
||||
<div id="modbus-fields">
|
||||
<div class="field"><label>IP-Adresse</label>
|
||||
<input type="text" id="modal-ip" placeholder="10.10.20.190"></div>
|
||||
<div class="field"><label>Modbus Port</label>
|
||||
<input type="number" id="modal-port" value="502"></div>
|
||||
<div class="field"><label>Modbus Slave-Adresse</label>
|
||||
<input type="number" id="modal-addr" value="1" min="1" max="247"></div>
|
||||
</div>
|
||||
<div id="ha-entity-fields" style="display:none">
|
||||
<div class="field"><label>HA Entity-Prefix</label>
|
||||
<input type="text" id="modal-ha-prefix" placeholder="growatt_wifi_x_1">
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:3px">Ohne "sensor."-Präfix — z.B. bei <code>sensor.growatt_wifi_x_1_dcpower</code> ist der Wert <code>growatt_wifi_x_1</code>. Braucht ein bereits per ESPHome in HA eingebundenes Gerät, keinen direkten Netzwerkzugriff.</div></div>
|
||||
</div>
|
||||
<div class="field"><label>MQTT Topic-Präfix</label>
|
||||
<input type="text" id="modal-prefix" placeholder="growatt/wechselrichter1"></div>
|
||||
<div class="field"><label>Abfrageintervall (Sekunden)</label>
|
||||
@@ -1036,7 +1043,9 @@ function renderInverterList() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-card-meta">
|
||||
📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}<br>
|
||||
${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}`}<br>
|
||||
📨 ${esc(inv.mqtt_topic_prefix)}<br>
|
||||
⏱ alle ${inv.update_interval || 30}s
|
||||
</div>
|
||||
@@ -1049,16 +1058,19 @@ function renderInverterList() {
|
||||
el.innerHTML = cards + `<div class="add-btn" onclick="openModal()">+ Gerät hinzufügen</div>`;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user