Skip to content

Commit

Permalink
Plugin to monitor Hyundai SUN2000 solar inverter via Modbus TCP
Browse files Browse the repository at this point in the history
  • Loading branch information
webstoney authored and kenyon committed Aug 20, 2024
1 parent bf9a4e4 commit 0a07c13
Showing 1 changed file with 225 additions and 0 deletions.
225 changes: 225 additions & 0 deletions plugins/solar/sun2000_
Original file line number Diff line number Diff line change
@@ -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_<ip-or-hostname> 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 <[email protected]>
# Copyright (c) 2023 Roland Steinbach <[email protected]>
#
# 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)

0 comments on commit 0a07c13

Please sign in to comment.