import logging import struct import time from typing import Dict, Optional from pymodbus.client import ModbusTcpClient from pymodbus.exceptions import ModbusException from inverters import Inverter, Sensor log = logging.getLogger(__name__) SLAVE = 0 # Kathrein: Unit-ID 0 (Broadcast) EMS_CTRL_REG = 0x00A0 EMS_RELAIS_REG = 0x00A1 EMS_CURRENT_REG = 0x00A2 EMS_ENABLE = 0x8000 MIN_CURRENT_MA = 6000 MAX_CURRENT_MA = 32000 _PHASE_MASK = {1: 0x0001, 2: 0x0003, 3: 0x0007} class WallboxReader: def __init__(self, host: str, port: int = 502, timeout: float = 10.0): self.host = host self.port = port self.timeout = timeout self._client: Optional[ModbusTcpClient] = None def _connect(self) -> bool: if self._client and self._client.connected: return True self._client = ModbusTcpClient(self.host, port=self.port, timeout=self.timeout) if not self._client.connect(): log.error("[Wallbox %s] Verbindung fehlgeschlagen", self.host) self._client = None return False return True def _disconnect(self): if self._client: self._client.close() self._client = None def read(self, inverter: Inverter) -> Optional[Dict[str, float]]: if not self._connect(): return None reg_cache: Dict[int, int] = {} any_ok = False for start, length in inverter.read_ranges: try: result = self._client.read_holding_registers(start, count=length, slave=SLAVE) if result.isError(): log.warning("[Wallbox] FC03 Reg 0x%04X nicht verfügbar: %s", start, result) continue for i, val in enumerate(result.registers): reg_cache[start + i] = val any_ok = True except ModbusException as e: log.error("[Wallbox] Modbus Ausnahme: %s", e) self._disconnect() return None return _extract(inverter.sensors, reg_cache) if any_ok else None def enable_ems(self) -> bool: if not self._connect(): return False try: r = self._client.write_register(EMS_CTRL_REG, EMS_ENABLE, slave=SLAVE) return not r.isError() except ModbusException as e: log.error("[Wallbox] EMS enable Fehler: %s", e) return False def set_current(self, ma: int, phases: int = 3) -> bool: """Ladestrom setzen. ma=0 → Pause, ma=0xFFFF → Abbruch.""" if not self._connect(): return False val = 0 if ma == 0 else max(MIN_CURRENT_MA, min(MAX_CURRENT_MA, ma)) relais = _PHASE_MASK.get(phases, 0x0007) try: r = self._client.write_register(EMS_RELAIS_REG, relais, slave=SLAVE) if r.isError(): log.warning("[Wallbox] Relais-Matrix Fehler: %s", r) r = self._client.write_register(EMS_CURRENT_REG, val, slave=SLAVE) ok = not r.isError() if ok: log.info("[Wallbox] Strom gesetzt: %d mA (%d Phase(n))", val, phases) return ok except ModbusException as e: log.error("[Wallbox] set_current Fehler: %s", e) return False def close(self): self._disconnect() def _extract(sensors: list, regs: Dict[int, int]) -> Dict[str, float]: values: Dict[str, float] = {} for s in sensors: if s.reg not in regs: continue dt = getattr(s, "data_type", "uint16") try: if dt == "float32": if s.reg + 1 not in regs: continue raw = struct.unpack(">f", struct.pack(">HH", regs[s.reg], regs[s.reg + 1]))[0] elif dt == "uint32": if s.reg + 1 not in regs: continue raw = (regs[s.reg] << 16) | regs[s.reg + 1] else: raw = regs[s.reg] values[s.id] = round(float(raw) * s.scale, 3) except Exception: pass return values