From 0a07c13394aae3ecb83bd3b702f92b77b253e530 Mon Sep 17 00:00:00 2001 From: RS Date: Tue, 31 Oct 2023 09:08:58 +0100 Subject: [PATCH] Plugin to monitor Hyundai SUN2000 solar inverter via Modbus TCP --- plugins/solar/sun2000_ | 225 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 plugins/solar/sun2000_ diff --git a/plugins/solar/sun2000_ b/plugins/solar/sun2000_ new file mode 100644 index 00000000..399b97bb --- /dev/null +++ b/plugins/solar/sun2000_ @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +# Wildcard-plugin to monitor a Huawei SUN 2000 inverter with SDongle +# through Modbus TCP +# +# To monitor an inverter, link sun2000_ to this file. +# E.g. +# ln -s /usr/share/munin/plugins/sun2000_ \ +# /etc/munin/plugins/sun2000_192.168.1.2 +# ...will monitor the dongle with ip 192.168.1.2 +# +# prerequisite is pymodbus library, install with 'pip3 install pymodbus[all]' +# +# Can have following configuration in plugin-conf.d/munin-node: +# [sun2000_*] +# env.MODBUS_PORT 502 +# env.HAS_BATTERY 0 +# env.ADDITIONAL_INVERTERS +# +# Parameters +# MODBUS_PORT - Specifies the TCP-Port of the sun2000 smart dongle. +# Defaults to 502 +# HAS_BATTERY - If the inverter has battery storage attached, set to 1 +# for additional graphs. Defaults to 0 +# ADDITIONAL_INVERTERS - if you have additional inverters attached, put +# their modbus id(s) comma-separated here (e.g. +# 16,32). Usually the first additional slave +# inverter does have id 16 +# +# The same parameters can also be specified on a per-inverter basis, eg: +# [sun2000_inverter1] +# env.MODBUS_PORT 503 +# +# Author: Roland Steinbach +# Copyright (c) 2023 Roland Steinbach +# +# Permission to use, copy, and modify this software with or without fee +# is hereby granted, provided that this entire notice is included in +# all source code copies of any software which is or includes a copy or +# modification of this software. +# +# THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR +# IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY +# REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE +# MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR +# PURPOSE. +# +# +# Magic markers +# #%# capabilities=autoconf +# #%# family=auto + + +import logging +from time import sleep +import os +import sys + +from pymodbus import pymodbus_apply_logging_config + +# --------------------------------------------------------------------------- # +# import the various client implementations +# --------------------------------------------------------------------------- # +from pymodbus.client import ModbusTcpClient +from pymodbus.transaction import ModbusSocketFramer + +plugin_name = list(os.path.split(sys.argv[0]))[1] +PORT = os.environ.get('MODBUS_PORT') or 502 +HASBATTERY = os.environ.get('HAS_BATTERY') or 0 +ADDINV = os.environ.get('ADDITIONAL_INVERTERS') or [] +if (len(ADDINV) != 0): + ADDINV = ADDINV.split(",") +plugin_version = "0.6" + + +def get_sun2000_ip(plugin_name): + try: + name = plugin_name.split('_', 1)[1] + return name + except Exception: + logging.verbose("IP not found!") + sys.exit(1) + + +def to_I32(i): + if (len(i) != 2): + return 0 + else: + i = ((i[0] << 16) + i[1]) + i = i & 0xffffffff + return (i ^ 0x80000000) - 0x80000000 + + +def to_U32(i): + if (len(i) != 2): + return 0 + else: + return ((i[0] << 16) + i[1]) + + +def to_str(s): + str = "" + for i in range(0, len(s)): + high, low = divmod(s[i], 0x100) + str = str + chr(high) + chr(low) + return str + + +def to_I16(i): + i = i[0] & 0xffff + return (i ^ 0x8000) - 0x8000 + + +def print_config(): + print("multigraph sun2000_green") + print("graph_title SUN2000 pct green power") + print("graph_order power1 pctgreen1") + print("graph_category sensors") + print("graph_vlabel Green Power (%)") + print("graph_args -l 0 -u 100") + print("graph_info Percentage of green power") + print("pctgreen1.label Green power (%)") + print("pctgreen1.colour 00cccc") + print("") + print("multigraph sun2000_plant") + print("graph_title SUN2000 PowerPlant") + print("graph_order power1 watt1 usage1") + print("graph_category sensors") + print("graph_vlabel Power (W)") + print("graph_scale yes") + print("graph_info Solar IN/Out Values in W. + = sending, - = getting") + print("power1.label Solar power (W)") + print("power1.colour 00cc00") + print("watt1.label Grid (W)") + print("watt1.colour cc3300") + print("usage1.label Used power (W)") + print("usage1.colour 0000cc") + if (HASBATTERY != 0): + print("bat1.label Battery (W)") + print("bat1.colour eeee00") + print("") + print("multigraph sun2000_battery") + print("graph_title SUN2000 storage battery") + print("graph_order pct1") + print("graph_category sensors") + print("graph_vlabel State of charge (%)") + print("graph_args -l 0 -u 100") + print("graph_info State of charge") + print("pct1.label State of charge (%)") + print("pct1.min 0") + print("pct1.max 100") + + +logging.basicConfig() +_logger = logging.getLogger(__file__) +_logger.setLevel(logging.ERROR) +pymodbus_apply_logging_config(logging.ERROR) +ip = get_sun2000_ip(plugin_name) + +client = ModbusTcpClient(ip, + port=PORT, + framer=ModbusSocketFramer, + timeout=5, + retry_on_empty=False, + close_comm_on_error=True,) +client.connect() +sleep(5) + +if len(sys.argv) > 1: + if sys.argv[1] == "config": + print_config() + sys.exit(0) + elif sys.argv[1] == "autoconf": + print('yes') + sys.exit(0) + elif sys.argv[1] == "version": + print('sun2000_ Munin plugin, version ' + plugin_version) + sys.exit(0) + elif sys.argv[1] != "": + logging.verbose('unknown argument "' + sys.argv[1] + '"') + sys.exit(1) + +if client.connect(): + power1 = 0 + bat1 = 0 + watt1 = 0 + usage1 = 0 + pctgreen1 = 0 + APPD = client.read_holding_registers(32064, 2, 1) # Power from solar + if hasattr(APPD, "registers"): + power1 = to_I32(APPD.registers) + if (len(ADDINV) > 0): + for i in range(0, len(ADDINV)): + APPD = client.read_holding_registers(32064, 2, int(ADDINV[i])) + if hasattr(APPD, "registers"): + power1 += to_I32(APPD.registers) + # Watt to/from Grid (>0 = feeding, <0 = getting) + APPD = client.read_holding_registers(37113, 2, 1) + if hasattr(APPD, "registers"): + watt1 = to_I32(APPD.registers) + if (HASBATTERY != 0): + # Watt to/from battery (>0 = charge, <0 = discharge) + APPD = client.read_holding_registers(37001, 2, 1) + if hasattr(APPD, "registers"): + bat1 = to_I32(APPD.registers) + APPD = client.read_holding_registers(37004, 1, 1) # battery SOC + if hasattr(APPD, "registers"): + pct1 = (to_I16(APPD.registers) / 10) + usage1 = abs(int(power1) - int(bat1) - int(watt1)) + if (watt1 > 0): + pctgreen1 = 100 + elif watt1 + usage1 > 0: + pctgreen1 = 100 - (abs(watt1) / usage1 * 100) + print("multigraph sun2000_green") + print("pctgreen1.value", pctgreen1) + print("") + print("multigraph sun2000_plant") + print("power1.value", -(power1)) + print("watt1.value", watt1) + print("usage1.value", usage1) + if (HASBATTERY != 0): + print("bat1.value", bat1) + print("") + print("multigraph sun2000_battery") + print("pct1.value", pct1)