Compare commits
14 Commits
82acfe4899
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 43b880b4f3 | |||
| ecb2810c3b | |||
| 971398505c | |||
| b649318aae | |||
| 61deb97761 | |||
| 49ad271301 | |||
| 36e39fe662 | |||
| 17c2309a28 | |||
| 0292bcf8d8 | |||
| 788a0de2c9 | |||
| f792f52d1f | |||
| 95fd9ab85d | |||
| 730d8c6f39 | |||
| ce21385a52 |
11
Modbus.md
Normal file
11
Modbus.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Victron GX Modbus configuration
|
||||||
|
|
||||||
|
Here is some screenshot on my Modbus configuration on my Victron Energy GX
|
||||||
|
|
||||||
|
Adapt the configuration of your plugins uppon YOUR GX system.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
82
README.md
82
README.md
@ -1,3 +1,85 @@
|
|||||||
# victron-energy-domoticz
|
# victron-energy-domoticz
|
||||||
|
|
||||||
Domoticz plugins for Victron Energy Multiplus and serveral modules that can be reached using modbus
|
Domoticz plugins for Victron Energy Multiplus and serveral modules that can be reached using modbus
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
You need to have a Victron Energy GX (what ever the one you have) and have setup
|
||||||
|
IP on the system.
|
||||||
|
|
||||||
|
See [Modbus Configuration](Modbus.md) for more information
|
||||||
|
|
||||||
|
## Installation of plugin
|
||||||
|
|
||||||
|
Install `pip3`:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
sudo apt install python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `domoticz`:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
curl -sSL install.domoticz.com | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure that the `plugins` folder exists in the `domoticz` folder.
|
||||||
|
|
||||||
|
Install the plugin:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
cd domoticz
|
||||||
|
git clone https://github.com/xbeaudouin/victron-energy-domoticz.git
|
||||||
|
cd plugins
|
||||||
|
ln -s ../victron-energy-domoticz/mppt .
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to the plugin folder and install all required addons:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
cd domoticz/plugins/mppt
|
||||||
|
sudo pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Once that is done, restart domoticz:
|
||||||
|
|
||||||
|
``` shell
|
||||||
|
sudo service domoticz.sh restart
|
||||||
|
```
|
||||||
|
|
||||||
|
## MPPT plugin
|
||||||
|
|
||||||
|
Once plugins installed, a new hardware will be available : 'Victron Energy MPPT over GX + Modbus"
|
||||||
|
|
||||||
|
To add the MPPT plugin add the following :
|
||||||
|
- Name : Victron_MPPT_1 (for example)
|
||||||
|
- GX IP Address : the IP address of your GX
|
||||||
|
- GX port Number : 502 should good (this is the default, but in case of this change)
|
||||||
|
- Modbus address : 229 (or depending of your setup you can have several MPPT on your system, adapt it as you need)
|
||||||
|
- If you want plenty of debug stuff (usefull to fix a bug) you can enable that.
|
||||||
|
|
||||||
|
### MPPT Screenshot
|
||||||
|
|
||||||
|
MPPT Setup
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
MPPT Devices
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
MPPT Voltage
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
MPPT Current
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
MPPT Power
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
MPPT kWh
|
||||||
|

|
||||||
|
|
||||||
|
|||||||
116
mppt/plugin.py
116
mppt/plugin.py
@ -7,7 +7,7 @@ Requirements:
|
|||||||
2. pymodbus AND pymodbusTCP
|
2. pymodbus AND pymodbusTCP
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
<plugin key="VictronEnergy_GX_MPPT" name="Victron Energy MPPT over GX + Modbus" author="Xavier Beaudouin" version="0.0.1" externallink="https://github.com/xbeaudouin/victron-energy-domoticz/mppt">
|
<plugin key="VictronEnergy_GX_MPPT" name="Victron Energy MPPT over GX + Modbus" author="Xavier Beaudouin" version="0.0.2" externallink="https://github.com/xbeaudouin/victron-energy-domoticz/mppt">
|
||||||
<params>
|
<params>
|
||||||
<param field="Address" label="GX IP Address" width="150px" required="true" />
|
<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" />
|
<param field="Port" label="GX Modbus Port Number" width="100px" required="true" default="502" />
|
||||||
@ -31,6 +31,7 @@ sys.path.append('/usr/local/lib/python3.6/dist-packages')
|
|||||||
sys.path.append('/usr/local/lib/python3.7/dist-packages')
|
sys.path.append('/usr/local/lib/python3.7/dist-packages')
|
||||||
sys.path.append('/usr/local/lib/python3.8/dist-packages')
|
sys.path.append('/usr/local/lib/python3.8/dist-packages')
|
||||||
sys.path.append('/usr/local/lib/python3.9/dist-packages')
|
sys.path.append('/usr/local/lib/python3.9/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.10/dist-packages')
|
||||||
|
|
||||||
import pymodbus
|
import pymodbus
|
||||||
|
|
||||||
@ -98,7 +99,6 @@ class Maximum:
|
|||||||
|
|
||||||
# Plugin itself
|
# Plugin itself
|
||||||
class BasePlugin:
|
class BasePlugin:
|
||||||
#enabled = False
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Voltage for last 5 minutes
|
# Voltage for last 5 minutes
|
||||||
self.voltage=Average()
|
self.voltage=Average()
|
||||||
@ -138,7 +138,6 @@ class BasePlugin:
|
|||||||
Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on device : "+str(self.MBAddr))
|
Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on device : "+str(self.MBAddr))
|
||||||
|
|
||||||
# Create the devices if they does not exists
|
# Create the devices if they does not exists
|
||||||
# TODO: refactor this.
|
|
||||||
if 1 not in Devices:
|
if 1 not in Devices:
|
||||||
Domoticz.Device(Name="Voltage", Unit=1, TypeName="Voltage", Used=0).Create()
|
Domoticz.Device(Name="Voltage", Unit=1, TypeName="Voltage", Used=0).Create()
|
||||||
if 2 not in Devices:
|
if 2 not in Devices:
|
||||||
@ -147,7 +146,6 @@ class BasePlugin:
|
|||||||
Options = { "Custom": "1;W" }
|
Options = { "Custom": "1;W" }
|
||||||
Domoticz.Device(Name="Power", Unit=3, TypeName="Custom", Used=0, Options=Options).Create()
|
Domoticz.Device(Name="Power", Unit=3, TypeName="Custom", Used=0, Options=Options).Create()
|
||||||
if 4 not in Devices:
|
if 4 not in Devices:
|
||||||
#Domoticz.Device(Name="Total Energy", Unit=4, Type=0xfa, Subtype=0x01, Used=0).Create()
|
|
||||||
Domoticz.Device(Name="Total Energy", Unit=4, Type=243, Subtype=29, Used=0).Create()
|
Domoticz.Device(Name="Total Energy", Unit=4, Type=243, Subtype=29, Used=0).Create()
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -156,22 +154,6 @@ class BasePlugin:
|
|||||||
def onStop(self):
|
def onStop(self):
|
||||||
Domoticz.Debugging(0)
|
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):
|
def onHeartbeat(self):
|
||||||
Domoticz.Debug(" Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr))
|
Domoticz.Debug(" Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr))
|
||||||
try:
|
try:
|
||||||
@ -184,73 +166,30 @@ class BasePlugin:
|
|||||||
Devices[3].Update(1, "0")
|
Devices[3].Update(1, "0")
|
||||||
Devices[4].Update(1, "0")
|
Devices[4].Update(1, "0")
|
||||||
|
|
||||||
# TODO: catch errors
|
|
||||||
total_e = "0"
|
total_e = "0"
|
||||||
power = "0"
|
power = "0"
|
||||||
|
|
||||||
# Voltage
|
# Voltage
|
||||||
data = client.read_holding_registers(776, 1)
|
value = round (getmodbus16(776, client) / 100.0, 3)
|
||||||
Domoticz.Debug("Data from register 776: "+str(data))
|
|
||||||
# Unsigned 16
|
|
||||||
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
|
|
||||||
# Value
|
|
||||||
value = decoder.decode_16bit_int()
|
|
||||||
# Scale factor / 100
|
|
||||||
value = round (value / 100.0, 3)
|
|
||||||
Domoticz.Debug("Value after conversion : "+str(value))
|
|
||||||
Domoticz.Debug("-> Calculating average")
|
|
||||||
self.voltage.update(value)
|
self.voltage.update(value)
|
||||||
value = self.voltage.get()
|
value = self.voltage.get()
|
||||||
Domoticz.Debug(" = {}".format(value))
|
|
||||||
Devices[1].Update(1, str(value))
|
Devices[1].Update(1, str(value))
|
||||||
|
|
||||||
# Current
|
# Current
|
||||||
data = client.read_holding_registers(777, 1)
|
value = round (getmodbus16(777,client) / 10.0, 3)
|
||||||
Domoticz.Debug("Data from register 777: "+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.current.update(value)
|
self.current.update(value)
|
||||||
value = self.current.get()
|
value = self.current.get()
|
||||||
Domoticz.Debug(" = {}".format(value))
|
|
||||||
Devices[2].Update(1, str(value))
|
Devices[2].Update(1, str(value))
|
||||||
|
|
||||||
# Power
|
# Power
|
||||||
data = client.read_holding_registers(789, 1)
|
value = round (getmodbus16(789, client) / 10.0, 3)
|
||||||
Domoticz.Debug("Data from register 789: "+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.power.update(value)
|
self.power.update(value)
|
||||||
value = self.power.get()
|
value = self.power.get()
|
||||||
Domoticz.Debug(" = {}".format(value))
|
|
||||||
Devices[3].Update(1, str(value))
|
Devices[3].Update(1, str(value))
|
||||||
power = str(value)
|
power = str(value)
|
||||||
|
|
||||||
# Total Energy
|
# Total Energy
|
||||||
data = client.read_holding_registers(790, 1)
|
total_e = str(getmodbus16(790,client)*100)
|
||||||
Domoticz.Debug("Data from register 790: "+str(data))
|
|
||||||
# Unsigned 32
|
|
||||||
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
|
|
||||||
# Value
|
|
||||||
value = decoder.decode_16bit_int()
|
|
||||||
Domoticz.Debug(" = {}".format(value))
|
|
||||||
# Scale * 100 (to have Wh)
|
|
||||||
total_e = str(value*100)
|
|
||||||
|
|
||||||
|
|
||||||
# Do insert data on counters
|
|
||||||
#Devices[4].Update(1, sValue=total_e+";0;0;0;"+power+";0")
|
|
||||||
Devices[4].Update(1, sValue=power+";"+total_e)
|
Devices[4].Update(1, sValue=power+";"+total_e)
|
||||||
|
|
||||||
|
|
||||||
@ -265,26 +204,6 @@ def onStop():
|
|||||||
global _plugin
|
global _plugin
|
||||||
_plugin.onStop()
|
_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():
|
def onHeartbeat():
|
||||||
global _plugin
|
global _plugin
|
||||||
_plugin.onHeartbeat()
|
_plugin.onHeartbeat()
|
||||||
@ -303,3 +222,26 @@ def DumpConfigToLog():
|
|||||||
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
|
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
|
||||||
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
|
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# get Modbug 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)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
1
multiplus/.gitignore
vendored
Normal file
1
multiplus/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
474
multiplus/plugin.py
Normal file
474
multiplus/plugin.py
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Victron Energy Multiplus II plugin
|
||||||
|
Author: Xavier Beaudouin
|
||||||
|
Requirements:
|
||||||
|
1. multiplus + GX
|
||||||
|
2. pymodbus AND pymodbusTCP
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
<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" />
|
||||||
|
<param field="Mode3" label="GX Modbus address" width="100px" required="true" default="100" />
|
||||||
|
<param field="Mode4" label="Multiplus Modbus address" width="100px" required="true" default="228" />
|
||||||
|
<param field="Mode5" label="Battery Modbus address" width="100px" required="true" default="225" />
|
||||||
|
<param field="Mode6" label="Debug" width="100px">
|
||||||
|
<options>
|
||||||
|
<option label="True" value="Debug"/>
|
||||||
|
<option label="False" value="Normal" default="true" />
|
||||||
|
</options>
|
||||||
|
</param>
|
||||||
|
</params>
|
||||||
|
</plugin>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Domoticz
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.append('/usr/local/lib/python3.4/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.5/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.6/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.7/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.8/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.9/dist-packages')
|
||||||
|
sys.path.append('/usr/local/lib/python3.10/dist-packages')
|
||||||
|
|
||||||
|
import pymodbus
|
||||||
|
|
||||||
|
from pyModbusTCP.client import ModbusClient
|
||||||
|
from pymodbus.constants import Endian
|
||||||
|
from pymodbus.payload import BinaryPayloadDecoder
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# The Average class can be used to calculate the average value based on a sliding window of samples.
|
||||||
|
# The number of samples stored depends on the interval used to collect the value from the inverter itself.
|
||||||
|
#
|
||||||
|
|
||||||
|
class Average:
|
||||||
|
|
||||||
|
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("Average: {} - {} values".format(self.get(), len(self.samples)))
|
||||||
|
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
# The Maximum class can be used to calculate the highest value based on a sliding window of samples.
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Plugin itself
|
||||||
|
class BasePlugin:
|
||||||
|
def __init__(self):
|
||||||
|
# AC IN Voltage for last 5 minutes
|
||||||
|
self.acInVoltage=Average()
|
||||||
|
# AC IN Current for last 5 minutes
|
||||||
|
self.acInCurrent=Average()
|
||||||
|
# AC IN Power for last 5 minutes
|
||||||
|
self.acInPower=Average()
|
||||||
|
# AC IN Frequency for last 5 minutes
|
||||||
|
self.acInFrequency=Average()
|
||||||
|
# AC Out Voltage for last 5 minutes
|
||||||
|
self.acOutVoltage=Average()
|
||||||
|
# AC Out Current for last 5 minutes
|
||||||
|
self.acOutCurrent=Average()
|
||||||
|
# AC Out Power for last 5 minutes
|
||||||
|
self.acOutPower=Average()
|
||||||
|
# AC Out Frequency for last 5 minutes
|
||||||
|
self.acOutFrequency=Average()
|
||||||
|
# Battery Voltage for last 5 minutes
|
||||||
|
self.batteryVoltage=Average()
|
||||||
|
# Battery Current for last 5 minutes
|
||||||
|
self.batteryCurrent=Average()
|
||||||
|
# Battery SOC for last 5 minutes
|
||||||
|
self.batterySoc=Average()
|
||||||
|
# Battery Temperature for last 5 minutes
|
||||||
|
self.batteryTemp=Average()
|
||||||
|
# Grid Power for last 5 minutes
|
||||||
|
self.gridpower=Average()
|
||||||
|
# Consumption for last 5 minutes
|
||||||
|
self.conso=Average()
|
||||||
|
# PV on output for last 5 minutes
|
||||||
|
self.pv=Average()
|
||||||
|
# Battery power on last 5 minutes
|
||||||
|
self.batteryPower=Average()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def onStart(self):
|
||||||
|
try:
|
||||||
|
Domoticz.Log("Victron Energy Multiplus-II Modbus loaded!, using python v" + sys.version[:6] + " and pymodbus v" + pymodbus.__version__)
|
||||||
|
except:
|
||||||
|
Domoticz.Log("Victron Energy Multiplus-II Modbus loaded!")
|
||||||
|
|
||||||
|
# Check dependancies
|
||||||
|
try:
|
||||||
|
if (float(Parameters["DomoticzVersion"][:6]) < float("2020.2")): Domoticz.Error("WARNING: Domoticz version is outdated or not supported. Please update!")
|
||||||
|
if (float(sys.version[:1]) < 3): Domoticz.Error("WARNING: Python3 should be used !")
|
||||||
|
if (float(pymodbus.__version__[:3]) < float("2.3")): Domoticz.Error("WARNING: pymodbus version is outdated, please update!")
|
||||||
|
except:
|
||||||
|
Domoticz.Error("Warning ! Dependancies could not be checked !")
|
||||||
|
|
||||||
|
# Parse parameters
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
if Parameters["Mode6"] == "Debug":
|
||||||
|
Domoticz.Debugging(1)
|
||||||
|
else:
|
||||||
|
Domoticz.Debugging(0)
|
||||||
|
|
||||||
|
self.IPAddress = Parameters["Address"]
|
||||||
|
self.IPPort = Parameters["Port"]
|
||||||
|
self.MBAddr = int(Parameters["Mode3"])
|
||||||
|
self.MultiAddr = int(Parameters["Mode4"])
|
||||||
|
self.BattAddr = int(Parameters["Mode5"])
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
# Multiplus Devices
|
||||||
|
if 1 not in Devices:
|
||||||
|
Domoticz.Device(Name="Voltage IN L1", Unit=1, TypeName="Voltage", Used=0).Create()
|
||||||
|
if 2 not in Devices:
|
||||||
|
Domoticz.Device(Name="Current IN L1", Unit=2, TypeName="Current (Single)", Used=0).Create()
|
||||||
|
if 3 not in Devices:
|
||||||
|
Options = { "Custom": "1;W" }
|
||||||
|
Domoticz.Device(Name="Power IN L1", Unit=3, TypeName="Custom", Used=0, Options=Options).Create()
|
||||||
|
if 4 not in Devices:
|
||||||
|
Options = { "Custom": "1;Hz" }
|
||||||
|
Domoticz.Device(Name="Frequency IN L1", Unit=4, TypeName="Custom", Used=0, Options=Options).Create()
|
||||||
|
if 5 not in Devices:
|
||||||
|
Domoticz.Device(Name="Voltage OUT L1", Unit=5, TypeName="Voltage", Used=0).Create()
|
||||||
|
if 6 not in Devices:
|
||||||
|
Domoticz.Device(Name="Current OUT L1", Unit=6, TypeName="Current (Single)", Used=0).Create()
|
||||||
|
if 7 not in Devices:
|
||||||
|
Options = { "Custom": "1;W" }
|
||||||
|
Domoticz.Device(Name="Power OUT L1", Unit=7, TypeName="Custom", Used=0, Options=Options).Create()
|
||||||
|
if 8 not in Devices:
|
||||||
|
Options = { "Custom": "1;Hz" }
|
||||||
|
Domoticz.Device(Name="Frequency OUT L1", Unit=8, TypeName="Custom", Used=0, Options=Options).Create()
|
||||||
|
if 9 not in Devices:
|
||||||
|
Domoticz.Device(Name="Grid Lost", Unit=9, TypeName="Alert", Used=0).Create()
|
||||||
|
if 10 not in Devices:
|
||||||
|
Domoticz.Device(Name="VE.Bus State", Unit=10, TypeName="Text", Used=0).Create()
|
||||||
|
|
||||||
|
# Battery
|
||||||
|
if 20 not in Devices:
|
||||||
|
Domoticz.Device(Name="Battery Voltage", Unit=20, TypeName="Voltage", Used=0).Create()
|
||||||
|
if 21 not in Devices:
|
||||||
|
Domoticz.Device(Name="Battery Current", Unit=21, TypeName="Current (Single)", Used=0).Create()
|
||||||
|
if 22 not in Devices:
|
||||||
|
Domoticz.Device(Name="Battery SOC", Unit=22, TypeName="Percentage", Used=0).Create()
|
||||||
|
if 23 not in Devices:
|
||||||
|
Domoticz.Device(Name="Battery Temperature", Unit=23, TypeName="Temperature", Used=0).Create()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
if 31 not in Devices:
|
||||||
|
Options = { "Custom": "1;W" }
|
||||||
|
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()
|
||||||
|
if 33 not in Devices:
|
||||||
|
Options = { "Custom": "1;W" }
|
||||||
|
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()
|
||||||
|
if 35 not in Devices:
|
||||||
|
Domoticz.Device(Name="ESS Battery Life SoC Limit", Unit=35, TypeName="Percentage", Used=0).Create()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def onStop(self):
|
||||||
|
Domoticz.Debugging(0)
|
||||||
|
|
||||||
|
def onHeartbeat(self):
|
||||||
|
|
||||||
|
# Multiplus devices
|
||||||
|
Domoticz.Debug("Multiplus Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MultiAddr))
|
||||||
|
try:
|
||||||
|
client = ModbusClient(host=self.IPAddress, port=self.IPPort, unit_id=self.MultiAddr, auto_open=True, auto_close=True, timeout=2)
|
||||||
|
except:
|
||||||
|
Domoticz.Error("Error connecting to TCP/Interface on address : "+self.IPaddress+":"+str(self.IPPort))
|
||||||
|
# Set value to 0 -> Error on all devices
|
||||||
|
Devices[1].Update(1, "0")
|
||||||
|
Devices[2].Update(1, "0")
|
||||||
|
Devices[3].Update(1, "0")
|
||||||
|
Devices[4].Update(1, "0")
|
||||||
|
Devices[5].Update(1, "0")
|
||||||
|
Devices[6].Update(1, "0")
|
||||||
|
Devices[7].Update(1, "0")
|
||||||
|
Devices[8].Update(1, "0")
|
||||||
|
Devices[9].Update(1, "0")
|
||||||
|
Devices[10].Update(1, "0")
|
||||||
|
|
||||||
|
# Ac In Voltage
|
||||||
|
self.acInVoltage.update(round(getmodbus16(3, client)/10.0, 3))
|
||||||
|
Devices[1].Update(1, self.acInVoltage.strget())
|
||||||
|
|
||||||
|
# Ac In Current
|
||||||
|
self.acInCurrent.update(round(getmodbus16(6, client)/10.0, 3))
|
||||||
|
Devices[2].Update(1, self.acInCurrent.strget())
|
||||||
|
|
||||||
|
# Ac In Power
|
||||||
|
self.acInPower.update(round(getmodbus16(12, client)/0.1, 3))
|
||||||
|
Devices[3].Update(1, self.acInPower.strget())
|
||||||
|
|
||||||
|
# Ac In Frequency
|
||||||
|
self.acInFrequency.update(round(getmodbus16(9, client)/100.0, 3))
|
||||||
|
Devices[4].Update(1, self.acInFrequency.strget())
|
||||||
|
|
||||||
|
# Ac Out Voltage
|
||||||
|
self.acOutVoltage.update(round(getmodbus16(15, client)/10.0, 3))
|
||||||
|
Devices[5].Update(1, self.acOutVoltage.strget())
|
||||||
|
|
||||||
|
# Ac Out Current
|
||||||
|
self.acOutCurrent.update(round(getmodbus16(18, client)/10.0, 3))
|
||||||
|
Devices[6].Update(1, self.acOutCurrent.strget())
|
||||||
|
|
||||||
|
# Ac Out Power
|
||||||
|
self.acOutPower.update(round(getmodbus16(23, client)/0.1, 3))
|
||||||
|
Devices[7].Update(1, self.acOutPower.strget())
|
||||||
|
|
||||||
|
# Ac Out Frequency
|
||||||
|
self.acOutFrequency.update(round(getmodbus16(21, client)/100.0, 3))
|
||||||
|
Devices[8].Update(1, self.acOutFrequency.strget())
|
||||||
|
|
||||||
|
# Grid lost
|
||||||
|
value = getmodbus16(61, client)
|
||||||
|
if value == 0:
|
||||||
|
Devices[9].Update(nValue=value, sValue="Ok")
|
||||||
|
elif value == 2:
|
||||||
|
Devices[9].Update(nValue=value, sValue="Alert - Grid Lost")
|
||||||
|
else:
|
||||||
|
Devices[9].Update(nValue=3, sValue="Unknown state ?")
|
||||||
|
|
||||||
|
# VE.Bus state
|
||||||
|
value = getmodbus16(31, client)
|
||||||
|
vebus = 'Unknown?'
|
||||||
|
if value == 0:
|
||||||
|
vebus = 'Off'
|
||||||
|
elif value == 1:
|
||||||
|
vebus = 'Low Power'
|
||||||
|
elif value == 2:
|
||||||
|
vebus = 'Fault'
|
||||||
|
elif value == 3:
|
||||||
|
vebus = 'Bulk'
|
||||||
|
elif value == 4:
|
||||||
|
vebus = 'Absorption'
|
||||||
|
elif value == 5:
|
||||||
|
vebus = 'Float'
|
||||||
|
elif value == 6:
|
||||||
|
vebus = 'Storage'
|
||||||
|
elif value == 7:
|
||||||
|
vebus = 'Equalize'
|
||||||
|
elif value == 8:
|
||||||
|
vebus = 'Passthru'
|
||||||
|
elif value == 9:
|
||||||
|
vebus = 'Inverting'
|
||||||
|
elif value == 10:
|
||||||
|
vebus = 'Power assist'
|
||||||
|
elif value == 11:
|
||||||
|
vebus = 'Power supply'
|
||||||
|
Devices[10].Update(1, str(value)+": "+vebus)
|
||||||
|
|
||||||
|
|
||||||
|
# Multiplus devices
|
||||||
|
Domoticz.Debug("Multiplus Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.BattAddr))
|
||||||
|
try:
|
||||||
|
battery = ModbusClient(host=self.IPAddress, port=self.IPPort, unit_id=self.BattAddr, auto_open=True, auto_close=True, timeout=2)
|
||||||
|
except:
|
||||||
|
Domoticz.Error("Error connecting to TCP/Interface on address : "+self.IPaddress+":"+str(self.IPPort))
|
||||||
|
# Set value to 0 -> Error on all devices
|
||||||
|
Devices[20].Update(1, "0")
|
||||||
|
Devices[21].Update(1, "0")
|
||||||
|
Devices[22].Update(1, "0")
|
||||||
|
Devices[23].Update(1, "0")
|
||||||
|
|
||||||
|
# Battery Voltage
|
||||||
|
self.batteryVoltage.update(round(getmodbus16(259, battery)/100.0, 3))
|
||||||
|
Devices[20].Update(1, self.batteryVoltage.strget())
|
||||||
|
|
||||||
|
# Battery Current
|
||||||
|
self.batteryCurrent.update(round(getmodbus16(261, battery)/10.0,3))
|
||||||
|
Devices[21].Update(1, self.batteryCurrent.strget())
|
||||||
|
|
||||||
|
# Battery SOC
|
||||||
|
self.batterySoc.update(round(getmodbus16(266, battery)/10.0,3))
|
||||||
|
Devices[22].Update(1, self.batterySoc.strget())
|
||||||
|
|
||||||
|
# Battery Temperature
|
||||||
|
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))
|
||||||
|
try:
|
||||||
|
victron = ModbusClient(host=self.IPAddress, port=self.IPPort, unit_id=self.MBAddr, auto_open=True, auto_close=True, timeout=2)
|
||||||
|
except:
|
||||||
|
Domoticz.Error("Error connecting to TCP/Interface on address : "+self.IPaddress+":"+str(self.IPPort))
|
||||||
|
# Set value to 0 -> Error on all devices
|
||||||
|
Devices[30].Update(1, "0")
|
||||||
|
Devices[31].Update(1, "0")
|
||||||
|
Devices[32].Update(1, "0")
|
||||||
|
Devices[33].Update(1, "0")
|
||||||
|
Devices[34].Update(1, "0")
|
||||||
|
Devices[35].Update(1, "0")
|
||||||
|
|
||||||
|
# Grid Power L1
|
||||||
|
self.gridpower.update(getmodbus16(820, victron))
|
||||||
|
Devices[30].Update(1, self.gridpower.strget())
|
||||||
|
|
||||||
|
# Consumption L1
|
||||||
|
self.conso.update(getmodbus16(817, victron))
|
||||||
|
Devices[31].Update(1, self.conso.strget())
|
||||||
|
|
||||||
|
# PV on Output
|
||||||
|
self.pv.update(getmodbus16(808, victron))
|
||||||
|
Devices[32].Update(1, self.pv.strget())
|
||||||
|
|
||||||
|
# Battery Power
|
||||||
|
self.batteryPower.update(getmodbus16(842, victron))
|
||||||
|
Devices[33].Update(1, self.batteryPower.strget())
|
||||||
|
|
||||||
|
# ESS Battery State
|
||||||
|
value = getmodbus16(2900, victron)
|
||||||
|
batterystate = "Unknown?"
|
||||||
|
onbattery = 0
|
||||||
|
if value == 0:
|
||||||
|
batterystate = "Unused, Battery Life Disabled"
|
||||||
|
elif value == 1:
|
||||||
|
batterystate = "Restarted"
|
||||||
|
elif value == 2:
|
||||||
|
batterystate = "Self-compsumption"
|
||||||
|
onbattery = 1
|
||||||
|
elif value == 3:
|
||||||
|
batterystate = "Self-compsumption, SoC exceeds 85%"
|
||||||
|
onbattery = 1
|
||||||
|
elif value == 4:
|
||||||
|
batterystate = "Self-compsumption, SoC at 100%"
|
||||||
|
onbattery = 1
|
||||||
|
elif value == 5:
|
||||||
|
batterystate = "Discharge disabled"
|
||||||
|
elif value == 6:
|
||||||
|
batterystate = "Force Charge"
|
||||||
|
elif value == 7:
|
||||||
|
batterystate = "Sustain"
|
||||||
|
elif value == 9:
|
||||||
|
batterystate = "Keep batteries charged"
|
||||||
|
elif value == 10:
|
||||||
|
batterystate = "Battery Life disabled"
|
||||||
|
elif value == 11:
|
||||||
|
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 "onbattery" variable
|
||||||
|
|
||||||
|
# ESS Battery Life SoC Limit
|
||||||
|
value = (getmodbus16(2903, victron) / 10.0)
|
||||||
|
Devices[35].Update(1, str(value))
|
||||||
|
|
||||||
|
global _plugin
|
||||||
|
_plugin = BasePlugin()
|
||||||
|
|
||||||
|
def onStart():
|
||||||
|
global _plugin
|
||||||
|
_plugin.onStart()
|
||||||
|
|
||||||
|
def onStop():
|
||||||
|
global _plugin
|
||||||
|
_plugin.onStop()
|
||||||
|
|
||||||
|
def onHeartbeat():
|
||||||
|
global _plugin
|
||||||
|
_plugin.onHeartbeat()
|
||||||
|
|
||||||
|
# Generic helper functions
|
||||||
|
def DumpConfigToLog():
|
||||||
|
for x in Parameters:
|
||||||
|
if Parameters[x] != "":
|
||||||
|
Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
|
||||||
|
Domoticz.Debug("Device count: " + str(len(Devices)))
|
||||||
|
for x in Devices:
|
||||||
|
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
|
||||||
|
Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'")
|
||||||
|
Domoticz.Debug("Device Name: '" + Devices[x].Name + "'")
|
||||||
|
Domoticz.Debug("Device nValue: " + str(Devices[x].nValue))
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
2
multiplus/requirements.txt
Normal file
2
multiplus/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pymodbus
|
||||||
|
pymodbusTCP
|
||||||
BIN
screenshots/gx-modbus1.png
Normal file
BIN
screenshots/gx-modbus1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
screenshots/gx-modbus2.png
Normal file
BIN
screenshots/gx-modbus2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
screenshots/gx-modbus3.png
Normal file
BIN
screenshots/gx-modbus3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
screenshots/gx-modbus4.png
Normal file
BIN
screenshots/gx-modbus4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
screenshots/gx-modbus5.png
Normal file
BIN
screenshots/gx-modbus5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Reference in New Issue
Block a user