Code: Select all
#!/usr/bin/env python3
"""Marstek Venus E 3.0 - Modbus TCP naar MQTT/Domoticz.
Domoticz IDX-koppelingen:
2463 batterijvermogen (W)
2459 batterij SOC (%)
2460 batterijspanning (V)
2461 totaal geladen energie (kWh)
2462 totaal ontladen energie (kWh)
Registerinterpretaties:
30000[1] : vermogen, signed 16-bit, W
30100[0] : spanning, /100 = V
30100[1] : stroom, signed 16-bit, /10 = A (alleen log/diagnose)
34001[1] : SOC, /10 = %
33000[0:4]: twee 32-bit tellers (high word, low word), /100 = kWh
Let op: de schaalfactoren en interpretaties van spanning en energietellers
zijn gebaseerd op de eerder onderzochte registerwaarden. Controleer ze aan
de hand van plausibiliteit en veranderingen in de Marstek voordat je ze als
definitief beschouwt. Stroom wordt nog niet naar Domoticz gepubliceerd.
"""
import json
import logging
import time
from datetime import datetime
import paho.mqtt.client as mqtt
from pymodbus.client import ModbusTcpClient
# Marstek Ethernet
MARSTEK_IP = "192.168.2.97"
MARSTEK_PORT = 502
MODBUS_DEVICE_ID = 1
MODBUS_TIMEOUT = 5
# MQTT
MQTT_HOST = "127.0.0.1"
MQTT_PORT = 1883
MQTT_CLIENT_ID = "marstek_reader"
MQTT_RAW_TOPIC = "marstek/raw"
DOMOTICZ_TOPIC = "domoticz/in"
# Domoticz IDX
IDX = {
"battery_power": 2463,
"battery_soc": 2459,
"battery_voltage": 2460,
"battery_energy_in": 2461,
"battery_energy_out": 2462,
}
INTERVAL_SECONDS = 5
# Adressen en aantallen zijn gelijk gehouden aan de eerder werkende reader.
REGISTERS = {
"30000": (30000, 2),
"30100": (30100, 2),
"34001": (34001, 3),
"33000": (33000, 4),
}
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def signed16(value):
"""Converteer een unsigned Modbus-register naar signed 16-bit."""
return value if value < 32768 else value - 65536
def uint32(high_word, low_word):
"""Combineer twee 16-bit registers: eerst high word, dan low word."""
return (int(high_word) << 16) | int(low_word)
def publish_domoticz(client, idx, value):
if not idx:
return
payload = {"idx": int(idx), "nvalue": 0, "svalue": str(value)}
info = client.publish(
DOMOTICZ_TOPIC, json.dumps(payload), qos=1, retain=False
)
info.wait_for_publish()
if info.rc != mqtt.MQTT_ERR_SUCCESS:
raise RuntimeError(f"MQTT-publicatie mislukt voor Domoticz IDX {idx}")
def read_register_blocks():
client = ModbusTcpClient(
MARSTEK_IP,
port=MARSTEK_PORT,
timeout=MODBUS_TIMEOUT,
retries=0,
)
try:
if not client.connect():
raise ConnectionError(
f"Marstek niet bereikbaar op {MARSTEK_IP}:{MARSTEK_PORT}"
)
data = {}
for name, (address, count) in REGISTERS.items():
response = client.read_holding_registers(
address=address,
count=count,
device_id=MODBUS_DEVICE_ID,
)
if response.isError():
raise RuntimeError(
f"Modbus-fout bij registerblok {address}: {response}"
)
data[name] = response.registers
time.sleep(0.3)
return data
finally:
client.close()
def decode_measurements(data):
"""Decodeer alleen de eerder onderzochte meetwaarden."""
power_w = signed16(data["30000"][1])
# Blok 30100: eerste woord spanning, tweede woord stroom.
voltage_v = data["30100"][0] / 100.0
current_a = signed16(data["30100"][1]) / 10.0
# Blok 34001: tweede woord is SOC in tienden van procenten.
soc_percent = data["34001"][1] / 10.0
# Blok 33000: twee 32-bit cumulatieve tellers.
energy_in_kwh = uint32(data["33000"][0], data["33000"][1]) / 100.0
energy_out_kwh = uint32(data["33000"][2], data["33000"][3]) / 100.0
return {
"battery_power": power_w,
"battery_soc": soc_percent,
"battery_voltage": voltage_v,
"battery_current": current_a,
"battery_energy_in": energy_in_kwh,
"battery_energy_out": energy_out_kwh,
}
def main():
mqtt_client = mqtt.Client(client_id=MQTT_CLIENT_ID)
mqtt_client.connect(MQTT_HOST, MQTT_PORT, 60)
mqtt_client.loop_start()
logging.info("Marstek Venus E 3.0-driver gestart")
logging.info("Modbus: %s:%s", MARSTEK_IP, MARSTEK_PORT)
logging.info("Domoticz IDX'en: %s", IDX)
try:
while True:
try:
data = read_register_blocks()
values = decode_measurements(data)
# Ruwe registers bewaren voor controle en verdere analyse.
raw_payload = {
"timestamp": datetime.now().isoformat(),
"registers": data,
"decoded": values,
}
info = mqtt_client.publish(
MQTT_RAW_TOPIC,
json.dumps(raw_payload),
qos=1,
retain=True,
)
info.wait_for_publish()
publish_domoticz(
mqtt_client, IDX["battery_power"], values["battery_power"]
)
publish_domoticz(
mqtt_client, IDX["battery_soc"], values["battery_soc"]
)
publish_domoticz(
mqtt_client, IDX["battery_voltage"], values["battery_voltage"]
)
publish_domoticz(
mqtt_client, IDX["battery_energy_in"], values["battery_energy_in"]
)
publish_domoticz(
mqtt_client, IDX["battery_energy_out"], values["battery_energy_out"]
)
logging.info(
"Marstek | vermogen: %s W | SOC: %.1f%% | "
"spanning: %.2f V | stroom: %.1f A | "
"geladen: %.2f kWh | ontladen: %.2f kWh",
values["battery_power"],
values["battery_soc"],
values["battery_voltage"],
values["battery_current"],
values["battery_energy_in"],
values["battery_energy_out"],
)
except Exception as error:
logging.exception("Marstek uitleesfout: %s", error)
time.sleep(INTERVAL_SECONDS)
except KeyboardInterrupt:
logging.info("Marstek-driver handmatig gestopt")
finally:
mqtt_client.loop_stop()
mqtt_client.disconnect()
if __name__ == "__main__":
main()