Feature: Kathrein Wallbox + EMS-Controller (v1.5.0)

- wallbox_client.py: WallboxReader FC03, EMS enable/set_current
- ems_controller.py: PV-Überschussladen + 4h-Timeout Zwangsladen bis 06:00
- inverters.py: KATHREIN_WALLBOX mit 18 Sensoren (Meter + EVSE + EMS)
- main.py: kathrein-Protokollzweig, _get_pv_surplus() aus laufenden Geräten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-04-28 12:02:59 +02:00
parent 8b65666e80
commit ac965dcfa6
5 changed files with 315 additions and 1 deletions
+40
View File
@@ -13,6 +13,8 @@ from inverters import INVERTERS
import history
from modbus_client import ModbusReader
from goodwe_client import GoodweReader
from wallbox_client import WallboxReader
from ems_controller import EmsController
from mqtt_publisher import MqttPublisher
logging.basicConfig(
@@ -139,6 +141,29 @@ def _compute_aggregates() -> Dict[str, float]:
)
return result
# ── EMS Hilfsfunktionen ───────────────────────────────────────
def _get_pv_surplus() -> float:
"""PV-Überschuss in Watt aus laufenden Geräten ermitteln.
Goodwe: active_power negativ = Einspeisung (Überschuss).
Growatt: power_to_grid positiv = Einspeisung.
"""
surplus = 0.0
with State.lock:
for inv_cfg in State.inverters_cfg:
d = State.inv_data.get(inv_cfg["id"], {})
if not d.get("modbus_ok") or not d.get("values"):
continue
v = d["values"]
# Goodwe: active_power < 0 bedeutet Einspeisung
if "active_power" in v:
surplus += max(0.0, -v["active_power"])
# Growatt
if "power_to_grid" in v:
surplus += max(0.0, v["power_to_grid"])
return surplus
# ── Poll Loop ─────────────────────────────────────────────────
def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
@@ -156,10 +181,16 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
return
host = inv_cfg["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 == "kathrein":
reader = WallboxReader(host=host, port=port)
ems = EmsController()
log.info("[%s] Poll-Loop: %s @ %s:%s (Kathrein EMS) alle %ds",
inv_id, inverter.name, host, port, interval)
else:
reader = ModbusReader(host=host, port=port, slave=slave)
log.info("[%s] Poll-Loop: %s @ %s:%s alle %ds",
@@ -184,6 +215,15 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
while not stop.is_set():
t0 = time.time()
values = reader.read(inverter)
# EMS: PV-Überschuss aus anderen Geräten holen und Ladestrom regeln
if ems is not None and values is not None:
pv_surplus = _get_pv_surplus()
charging_state = int(values.get("charging_state", 0))
ems_status = ems.update(reader, pv_surplus, charging_state)
values["ems_status_code"] = float(charging_state)
log.debug("[%s] EMS: %s", inv_id, ems_status)
with State.lock:
d = State.inv_data.setdefault(inv_id, {"poll_count": 0})
if values is not None: