Added ESS Battery Life SoC Limit (GX >= 2.80 ONLY !)

Added modbus retry (2 times max) if there is an error during decode of message (this can be rare but)
Refactor the code using the modbus function
Added string converted data from Average() class
Commented the Max Class since it is not used in this module
Bumped version
This commit is contained in:
2022-10-21 12:08:42 +02:00
parent b649318aae
commit 971398505c

View File

@ -7,7 +7,7 @@ Requirements:
2. pymodbus AND pymodbusTCP
"""
"""
<plugin key="VictronEnergy_MultiplusII" name="Victron Energy Multiplus II + Modbus" author="Xavier Beaudouin" version="0.0.1" externallink="https://github.com/xbeaudouin/victron-energy-domoticz/mppt">
<plugin key="VictronEnergy_MultiplusII" name="Victron Energy Multiplus II + Modbus" author="Xavier Beaudouin" version="0.0.2" externallink="https://github.com/xbeaudouin/victron-energy-domoticz/mppt">
<params>
<param field="Address" label="GX IP Address" width="150px" required="true" />
<param field="Port" label="GX Modbus Port Number" width="100px" required="true" default="502" />
@ -70,6 +70,10 @@ class Average:
def get(self):
return sum(self.samples) / len(self.samples)
def strget(self):
return str(sum(self.samples) / len(self.samples))
#
# Domoticz shows graphs with intervals of 5 minutes.
# When collecting information from the inverter more frequently than that, then it makes no sense to only show the last value.
@ -78,30 +82,29 @@ class Average:
# The number of samples stored depends on the interval used to collect the value from the inverter itself.
#
class Maximum:
def __init__(self):
self.samples = []
self.max_samples = 30
def set_max_samples(self, max):
self.max_samples = max
if self.max_samples < 1:
self.max_samples = 1
def update(self, new_value, scale = 0):
self.samples.append(new_value * (10 ** scale))
while (len(self.samples) > self.max_samples):
del self.samples[0]
Domoticz.Debug("Maximum: {} - {} values".format(self.get(), len(self.samples)))
def get(self):
return max(self.samples)
#class Maximum:
#
# def __init__(self):
# self.samples = []
# self.max_samples = 30
#
# def set_max_samples(self, max):
# self.max_samples = max
# if self.max_samples < 1:
# self.max_samples = 1
#
# def update(self, new_value, scale = 0):
# self.samples.append(new_value * (10 ** scale))
# while (len(self.samples) > self.max_samples):
# del self.samples[0]
#
# Domoticz.Debug("Maximum: {} - {} values".format(self.get(), len(self.samples)))
#
# def get(self):
# return max(self.samples)
# Plugin itself
class BasePlugin:
#enabled = False
def __init__(self):
# AC IN Voltage for last 5 minutes
self.acInVoltage=Average()
@ -170,7 +173,6 @@ class BasePlugin:
Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on GX device : "+str(self.MBAddr)+" Multi Device : "+str(self.MultiAddr)+" and Battery : "+str(self.BattAddr))
# Create the devices if they does not exists
# TODO: refactor this.
# Multiplus Devices
if 1 not in Devices:
Domoticz.Device(Name="Voltage IN L1", Unit=1, TypeName="Voltage", Used=0).Create()
@ -210,18 +212,20 @@ class BasePlugin:
# Victron
if 30 not in Devices:
Options = { "Custom": "1;W" }
Domoticz.Device(Name="Grid Power L1", Unit=30, TypeName="Custom", Used=0, Options=Options).Create()
Domoticz.Device(Name="Grid Power L1", Unit=30, TypeName="Custom", Used=0, Options=Options).Create()
if 31 not in Devices:
Options = { "Custom": "1;W" }
Domoticz.Device(Name="Consumption L1", Unit=31, TypeName="Custom", Used=0, Options=Options).Create()
Domoticz.Device(Name="Consumption L1", Unit=31, TypeName="Custom", Used=0, Options=Options).Create()
if 32 not in Devices:
Options = { "Custom": "1;W" }
Domoticz.Device(Name="PV on Output", Unit=32, TypeName="Custom", Used=0, Options=Options).Create()
Domoticz.Device(Name="PV on Output", Unit=32, TypeName="Custom", Used=0, Options=Options).Create()
if 33 not in Devices:
Options = { "Custom": "1;W" }
Domoticz.Device(Name="Battery Power", Unit=33, TypeName="Custom", Used=0, Options=Options).Create()
Domoticz.Device(Name="Battery Power", Unit=33, TypeName="Custom", Used=0, Options=Options).Create()
if 34 not in Devices:
Domoticz.Device(Name="ESS Battery Life State", Unit=34, TypeName="Text", Used=0).Create()
Domoticz.Device(Name="ESS Battery Life State", Unit=34, TypeName="Text", Used=0).Create()
if 35 not in Devices:
Domoticz.Device(Name="ESS Battery Life SoC Limit", Unit=35, TypeName="Percentage", Used=0).Create()
return
@ -229,22 +233,6 @@ class BasePlugin:
def onStop(self):
Domoticz.Debugging(0)
def onConnect(self, Connection, Status, Description):
Domoticz.Log("onConnect called")
return
def onMessage(self, Connection, Data):
Domoticz.Log("onMessage called")
def onCommand(self, Unit, Command, Level, Hue):
Domoticz.Log("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
Domoticz.Log("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onDisconnect(self, Connection):
Domoticz.Log("onDisconnect called")
def onHeartbeat(self):
# Multiplus devices
@ -266,138 +254,39 @@ class BasePlugin:
Devices[10].Update(1, "0")
# Ac In Voltage
data = client.read_holding_registers(3, 1)
Domoticz.Debug("Data from register 3: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acInVoltage.update(value)
value = self.acInVoltage.get()
Domoticz.Debug(" = {}".format(value))
Devices[1].Update(1, str(value))
self.acInVoltage.update(round(getmodbus16(3, client)/10.0, 3))
Devices[1].Update(1, self.acInVoltage.strget())
# Ac In Current
data = client.read_holding_registers(6, 1)
Domoticz.Debug("Data from register 6: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acInCurrent.update(value)
value = self.acInCurrent.get()
Domoticz.Debug(" = {}".format(value))
Devices[2].Update(1, str(value))
self.acInCurrent.update(round(getmodbus16(6, client)/10.0, 3))
Devices[2].Update(1, self.acInCurrent.strget())
# Ac In Power
data = client.read_holding_registers(12, 1)
Domoticz.Debug("Data from register 12: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 0.1
value = round (value / 0.1, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acInPower.update(value)
value = self.acInPower.get()
Domoticz.Debug(" = {}".format(value))
Devices[3].Update(1, str(value))
self.acInPower.update(round(getmodbus16(12, client)/0.1, 3))
Devices[3].Update(1, self.acInPower.strget())
# Ac In Frequency
data = client.read_holding_registers(9, 1)
Domoticz.Debug("Data from register 9: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acInFrequency.update(value)
value = self.acInFrequency.get()
Domoticz.Debug(" = {}".format(value))
Devices[4].Update(1, str(value))
self.acInFrequency.update(round(getmodbus16(9, client)/100.0, 3))
Devices[4].Update(1, self.acInFrequency.strget())
# Ac Out Voltage
data = client.read_holding_registers(15, 1)
Domoticz.Debug("Data from register 15: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acOutVoltage.update(value)
value = self.acOutVoltage.get()
Domoticz.Debug(" = {}".format(value))
Devices[5].Update(1, str(value))
self.acOutVoltage.update(round(getmodbus16(15, client)/10.0, 3))
Devices[5].Update(1, self.acOutVoltage.strget())
# Ac Out Current
data = client.read_holding_registers(18, 1)
Domoticz.Debug("Data from register 18: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acOutCurrent.update(value)
value = self.acOutCurrent.get()
Domoticz.Debug(" = {}".format(value))
Devices[6].Update(1, str(value))
self.acOutCurrent.update(round(getmodbus16(18, client)/10.0, 3))
Devices[6].Update(1, self.acOutCurrent.strget())
# Ac Out Power
data = client.read_holding_registers(23, 1)
Domoticz.Debug("Data from register 23: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 0.1
value = round (value / 0.1, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acOutPower.update(value)
value = self.acOutPower.get()
Domoticz.Debug(" = {}".format(value))
Devices[7].Update(1, str(value))
self.acOutPower.update(round(getmodbus16(23, client)/0.1, 3))
Devices[7].Update(1, self.acOutPower.strget())
# Ac Out Frequency
data = client.read_holding_registers(21, 1)
Domoticz.Debug("Data from register 21: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.acOutFrequency.update(value)
value = self.acOutFrequency.get()
Domoticz.Debug(" = {}".format(value))
Devices[8].Update(1, str(value))
self.acOutFrequency.update(round(getmodbus16(21, client)/100.0, 3))
Devices[8].Update(1, self.acOutFrequency.strget())
# Grid lost
data = client.read_holding_registers(64, 1)
Domoticz.Debug("Data from register 64: "+str(data))
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
value = decoder.decode_16bit_int()
value = getmodbus16(61, client)
if value == 0:
Devices[9].Update(nValue=value, sValue="Ok")
elif value == 2:
@ -406,10 +295,7 @@ class BasePlugin:
Devices[9].Update(nValue=3, sValue="Unknown state ?")
# VE.Bus state
data = client.read_holding_registers(31, 1)
Domoticz.Debug("Data from register 31: "+str(data))
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
value = decoder.decode_16bit_int()
value = getmodbus16(31, client)
vebus = 'Unknown?'
if value == 0:
vebus = 'Off'
@ -451,68 +337,20 @@ class BasePlugin:
Devices[23].Update(1, "0")
# Battery Voltage
data = battery.read_holding_registers(259, 1)
Domoticz.Debug("Data from register 259: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.batteryVoltage.update(value)
value = self.batteryVoltage.get()
Domoticz.Debug(" = {}".format(value))
Devices[20].Update(1, str(value))
self.batteryVoltage.update(round(getmodbus16(259, battery)/100.0, 3))
Devices[20].Update(1, self.batteryVoltage.strget())
# Battery Current
data = battery.read_holding_registers(261, 1)
Domoticz.Debug("Data from register 261: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.batteryCurrent.update(value)
value = self.batteryCurrent.get()
Domoticz.Debug(" = {}".format(value))
Devices[21].Update(1, str(value))
self.batteryCurrent.update(round(getmodbus16(261, battery)/10.0,3))
Devices[21].Update(1, self.batteryCurrent.strget())
# Battery SOC
data = battery.read_holding_registers(266, 1)
Domoticz.Debug("Data from register 266: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.batterySoc.update(value)
value = self.batterySoc.get()
Domoticz.Debug(" = {}".format(value))
Devices[22].Update(1, str(value))
self.batterySoc.update(round(getmodbus16(266, battery)/10.0,3))
Devices[22].Update(1, self.batterySoc.strget())
# Battery Temperature
data = battery.read_holding_registers(262, 1)
Domoticz.Debug("Data from register 262: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.batteryTemp.update(value)
value = self.batteryTemp.get()
Domoticz.Debug(" = {}".format(value))
Devices[23].Update(1, str(value))
self.batteryTemp.update(round(getmodbus16(262, battery)/10.0,3))
Devices[23].Update(1, self.batteryTemp.strget())
# Victron devices
Domoticz.Debug("Multiplus Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr))
@ -526,78 +364,26 @@ class BasePlugin:
Devices[32].Update(1, "0")
Devices[33].Update(1, "0")
Devices[34].Update(1, "0")
Devices[35].Update(1, "0")
# Grid Power L1
data = victron.read_holding_registers(820, 1)
Domoticz.Debug("Data from register 820: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
##value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.gridpower.update(value)
value = self.gridpower.get()
Domoticz.Debug(" = {}".format(value))
Devices[30].Update(1, str(value))
self.gridpower.update(getmodbus16(820, victron))
Devices[30].Update(1, self.gridpower.strget())
# Consumption L1
data = victron.read_holding_registers(817, 1)
Domoticz.Debug("Data from register 817: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
#value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.conso.update(value)
value = self.conso.get()
Domoticz.Debug(" = {}".format(value))
Devices[31].Update(1, str(value))
self.conso.update(getmodbus16(817, victron))
Devices[31].Update(1, self.conso.strget())
# PV on Output
data = victron.read_holding_registers(808, 1)
Domoticz.Debug("Data from register 808: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100.0
#value = round (value / 100.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.pv.update(value)
value = self.pv.get()
Domoticz.Debug(" = {}".format(value))
Devices[32].Update(1, str(value))
self.pv.update(getmodbus16(808, victron))
Devices[32].Update(1, self.pv.strget())
# Battery Power
data = victron.read_holding_registers(842, 1)
Domoticz.Debug("Data from register 262: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10.0
##value = round (value / 10.0, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.batteryPower.update(value)
value = self.batteryPower.get()
Domoticz.Debug(" = {}".format(value))
Devices[33].Update(1, str(value))
self.batteryPower.update(getmodbus16(842, victron))
Devices[33].Update(1, self.batteryPower.strget())
# ESS Battery State
data = victron.read_holding_registers(2900, 1)
Domoticz.Debug("Data from register 2900: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
value = getmodbus16(2900, victron)
batterystate = "Unknown?"
onbattery = 0
if value == 0:
@ -627,7 +413,11 @@ class BasePlugin:
batterystate = "Battery Life disabled (low SoC)"
Devices[34].Update(1, str(value)+": "+batterystate)
# TODO: add a device to say on battery yes/no
# use the "onbatteryy" variable
# use the "onbattery" variable
# ESS Battery Life SoC Limit
value = (getmodbus16(2903, victron) / 10.0)
Devices[35].Update(1, str(value))
global _plugin
_plugin = BasePlugin()
@ -640,26 +430,6 @@ def onStop():
global _plugin
_plugin.onStop()
def onConnect(Connection, Status, Description):
global _plugin
_plugin.onConnect(Connection, Status, Description)
def onMessage(Connection, Data):
global _plugin
_plugin.onMessage(Connection, Data)
def onCommand(Unit, Command, Level, Hue):
global _plugin
_plugin.onCommand(Unit, Command, Level, Hue)
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
global _plugin
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
def onDisconnect(Connection):
global _plugin
_plugin.onDisconnect(Connection)
def onHeartbeat():
global _plugin
_plugin.onHeartbeat()
@ -678,3 +448,25 @@ def DumpConfigToLog():
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
return
# get Modbus 16 bits values
def getmodbus16(register, client):
value = 0
try:
data = client.read_holding_registers(register, 1)
Domoticz.Debug("Data from register "+str(register)+": "+str(data))
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
value = decoder.decode_16bit_int()
except:
Domoticz.Error("Error getting data from "+str(register) + ", try 1")
try:
data = client.read_holding_registers(register, 1)
Domoticz.Debug("Data from register "+str(register)+": "+str(data))
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
value = decoder.decode_16bit_int()
except:
Domoticz.Error("Error getting data from "+str(register) + ", try 2")
return value