mirror of
https://github.com/munin-monitoring/contrib.git
synced 2025-07-21 18:41:03 +00:00
Merge pull request #1396 from webstoney/master
SUN2000 solar inverter and battery stats
This commit is contained in:
commit
2212acdc16
1 changed files with 225 additions and 0 deletions
225
plugins/solar/sun2000_
Normal file
225
plugins/solar/sun2000_
Normal file
|
@ -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 <roland@netzblick.de>
|
||||||
|
# Copyright (c) 2023 Roland Steinbach <roland@netzblick.de>
|
||||||
|
#
|
||||||
|
# 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)
|
Loading…
Add table
Add a link
Reference in a new issue