NEWS
Sofar Solar HYD10 KTL Wechselrichter an modbus Adapter
-
https://www.solarvie.at/en-eu/products/sofarsolar-ethernet-dongle-lse-3
kaufe dir einfach dieses Teil und du hast Ruhe, 55 Euro kostet weniger als die Nerven die du lassen musst. Vor allem hast du es gleich auch noch in der App
-
Habe mir eine eigene Lösung geschrieben, welche ich euch nicht vorenthalten möchte.
Habe in einem LXC, welcher für diverse Skripte zuständig ist, folgendes Python Script drauf gemacht plus eine config.yaml .
Dies sendet die Sofar-Daten per MQTT an den ioBroker./opt/modbus-mqtt/modbus_tcp_rtu.py
#!/usr/bin/env python3 import socket import struct import time import yaml import logging import paho.mqtt.client as mqtt import os CONFIG_FILE = "/opt/modbus-mqtt/config.yaml" LOG_FILE = "/var/log/modbus-mqtt.log" logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" ) logging.info("Starting modbus-mqtt with config from disk") try: with open(CONFIG_FILE, "r") as f: cfg = yaml.safe_load(f) except Exception as e: logging.error(f"Failed to load config: {e}") exit(1) last_mtime = os.path.getmtime(CONFIG_FILE) MODBUS_HOST = cfg["modbus"]["host"] MODBUS_PORT = cfg["modbus"]["port"] UNIT_ID = cfg["modbus"]["unit_id"] MQTT_HOST = cfg["mqtt"]["host"] MQTT_PORT = cfg["mqtt"]["port"] BASE_TOPIC = cfg["mqtt"]["base_topic"] POLL_INTERVAL = cfg["poll_interval"] REGISTERS = cfg["registers"] mqttc = mqtt.Client() mqttc.username_pw_set(cfg["mqtt"]["username"], cfg["mqtt"]["password"]) mqttc.connect(MQTT_HOST, MQTT_PORT) mqttc.loop_start() # --------------------------------------------------------- # CRC16 Modbus # --------------------------------------------------------- def crc16(data): crc = 0xFFFF for pos in data: crc ^= pos for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc.to_bytes(2, byteorder="little") # --------------------------------------------------------- # RTU‑Frame senden über TCP # --------------------------------------------------------- def read_holding_registers_rtu_tcp(unit, address, count): # Build RTU frame: [unit][function][addr_hi][addr_lo][count_hi][count_lo][crc_lo][crc_hi] frame = bytearray() frame.append(unit) frame.append(0x03) # Function code: Read Holding Registers frame += struct.pack(">HH", address, count) frame += crc16(frame) # TCP socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s.connect((MODBUS_HOST, MODBUS_PORT)) s.send(frame) # Response response = s.recv(256) s.close() # Validate minimum length if len(response) < 5: raise Exception("Invalid response length") # Validate CRC data = response[:-2] crc_received = response[-2:] crc_calc = crc16(data) if crc_received != crc_calc: raise Exception("CRC mismatch") # Byte count byte_count = response[2] if byte_count != count * 2: raise Exception("Unexpected byte count") # Extract registers registers = [] for i in range(count): hi = response[3 + i*2] lo = response[4 + i*2] registers.append((hi << 8) | lo) return registers # --------------------------------------------------------- # Poll‑Loop # --------------------------------------------------------- while True: # Auto‑Reload wenn config.yaml geändert wurde current_mtime = os.path.getmtime(CONFIG_FILE) if current_mtime != last_mtime: logging.info("Config changed, restarting...") mqttc.loop_stop() exit(0) for reg in REGISTERS: addr = reg["addr"] reg_type = reg["type"] factor = reg["factor"] length = 2 if reg_type == "uint32" else 1 try: regs = read_holding_registers_rtu_tcp(UNIT_ID, addr, length) if reg_type == "int16": value = struct.unpack(">h", struct.pack(">H", regs[0]))[0] elif reg_type == "uint16": value = regs[0] elif reg_type == "uint32": value = (regs[0] << 16) | regs[1] else: continue value *= factor topic = f"{BASE_TOPIC}/register/{addr}" mqttc.publish(topic, value) #logging.info(f"Published {topic} = {value}") except Exception as e: logging.error(f"Error reading register {addr}: {e}") time.sleep(0.2) time.sleep(POLL_INTERVAL)/opt/modbus-mqtt/config.yaml
modbus: host: "192.168.88.101" port: 8899 unit_id: 1 mqtt: host: "192.168.1.251" port: 1883 base_topic: "modbus/inverter" username: "mqtt_user" password: "mqtt_passwort" poll_interval: 20 registers: - addr: 1199 name: "ActivePower_Load_Sys" type: "uint16" factor: 0.01 - addr: 1160 name: "ActivePower_PCC_Total" type: "int16" factor: 0.01 - addr: 1476 name: "Power_PV_Total" type: "uint16" factor: 0.01 - addr: 1542 name: "Power_Bat1" type: "int16" factor: 0.01 - addr: 1544 name: "SOC_Bat1" type: "uint16" factor: 1 - addr: 1668 name: "PV_Generation_Today" type: "uint32" factor: 0.01Beim Waveshare musste ich auf "Transparent" (anstelle von "modbus TCP <=> modbus RTU") stellen.
In der config seht ihr auch, dass LXCs und der Waveshare nicht im gleichen Netzwerk sind. WAN von 192.168.88.0 (MikroTik Router) hängt am Netz 192.168.1.0 (Fritzbox). Da hab ich eine Route definiert, damit man vom einen ins andere Netz zugreifen kann.
Die IPs, Ports, Usernames und Passwörter müsst ihr natürlich auf eure Gegebenheiten anpassen.In der /etc/systemd/system/modbus-mqtt.service
[Unit] Description=Modbus RTU over TCP to MQTT Gateway After=network-online.target Wants=network-online.target [Service] Type=simple User=root WorkingDirectory=/opt/modbus-mqtt ExecStart=/usr/bin/env python3 /opt/modbus-mqtt/modbus_tcp_rtu.py Restart=always RestartSec=5 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.targetWenn alles fertig ist, eingeben:
sudo systemctl daemon-reload sudo systemctl enable modbus-mqtt.service sudo systemctl start modbus-mqtt.serviceDie Daten werden nun per MQTT an den ioBroker gesendet.
Der Datenpunkt sieht dann etwa so aus:mqtt.0.modbus.inverter.register.{Registeradresse}
