Compare commits
16 Commits
8dd681bfb0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 30c785221e | |||
| e0393a6353 | |||
| ef88dd5f8c | |||
| fb5d47f25c | |||
| 3921bf4689 | |||
| 9b40e23293 | |||
| 92bb2a27dc | |||
| 00300e70e3 | |||
| eaf1087a0e | |||
| f6ceba6605 | |||
| 24ab8a96e4 | |||
| 250622a4f2 | |||
| 6442f4d1a8 | |||
| 63a697d4f4 | |||
| 05dd708d3b | |||
| edfb665a35 |
4
LICENSE
4
LICENSE
@ -1,5 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Xavier BEAUDOUIN
|
||||
Based on some work from
|
||||
Copyright (c) 2020 Addie Janssen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@ -18,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
105
README.md
105
README.md
@ -1,6 +1,6 @@
|
||||
# Domoticz DS238-2 ModbusTCP plugin
|
||||
# Domoticz DS238-2 ZN/S ModbusTCP plugin
|
||||
|
||||
A Domoticz plugin to collect data from Hiking DS238-2 D/ZN power meter ModbusTCP.
|
||||
A Domoticz plugin to collect data from Hiking DS238-2 ZN/S power meter ModbusTCP.
|
||||
|
||||
## Requirements
|
||||
|
||||
@ -50,17 +50,14 @@ sudo service domoticz.sh restart
|
||||
|
||||
## Configuration in Domoticz
|
||||
|
||||
Once the plugin is installed, a new hardware type will be available: `DS238-2 D/ZN ModbusTCP`.
|
||||
Once the plugin is installed, a new hardware type will be available: `DS238-2 ZN/S ModbusTCP`.
|
||||
|
||||
To add the inverter, go to `Setup` -> `Hardware` and add the counter:
|
||||
|
||||
- Enter a `name` for the counter.
|
||||
- Select `DS238-2 D/ZN ModbusTCP` from the `type` dropdown list.
|
||||
- Enter the IP address of the PW21 in the `Inverter IP Address` field.
|
||||
- Enter the port number (default: 502) of the inverter in the `Inverter Port Number` field.
|
||||
- Select `Yes` in the `Add missing devices` to create the devices when the counter is added. Select `No` after deleting unwanted devices. Leaving the option set to `Yes` will recreate the deleted devices once Domoticz is restarted.
|
||||
- Select an `Interval` (default: 10 seconds); this defines how often the plugin will collect the data from the counter. Short intervals will result in more accurate values and graphs, but also result in more network traffic and a higher workload for both Domoticz and the counter.
|
||||
- Optionally change the `Log level`; this defaults to `Normal`. When selecting `Extra`, the plugin will print all the information it received from the counter in the log.
|
||||
- Select `DS238-2 ZN/S ModbusTCP` from the `type` dropdown list.
|
||||
- Enter the IP address of the PW21 in the `IP Address` field.
|
||||
- Enter the port number (default: 502) of the PW21 in the `Port Number` field.
|
||||
- Optionally turn on `Debug`; be aware: this will generate a lot of entries in the Domoticz log!
|
||||
- `Add` the counter.
|
||||
|
||||
@ -80,4 +77,94 @@ Once that is done, restart domoticz:
|
||||
``` shell
|
||||
sudo service domoticz.sh restart
|
||||
```
|
||||
## ScreenShots
|
||||
|
||||
Hardware page showing a configured PW21 to get data from a DS238-2 ZN/S
|
||||
|
||||

|
||||
|
||||
The devices page show all Domoticz devices that were created for it.
|
||||
|
||||

|
||||
|
||||
The voltage graphs.
|
||||
|
||||

|
||||
|
||||
Total power meter with Return energy (usefull for PV/Wind turbine generators)
|
||||
|
||||

|
||||
|
||||
## Documentation about the registers used.
|
||||
|
||||
This is a copy of : https://gist.github.com/alphp/95e1efe916c0dd6df7156f43dd521d53
|
||||
|
||||
### Modbus holding registers:
|
||||
|
||||
| Register(s) | Meaning | Scale Unit | Data format | R/W |
|
||||
|-------------|-----------------|------------|----------------|:---:|
|
||||
| 0000h-0001h | total energy | 1/100 kWh | unsigned dword | R<> |
|
||||
| 0002h-0003h | reserved | | unsigned dword | |
|
||||
| 0004h-0005h | reserved | | unsigned dword | |
|
||||
| 0006h-0007h | reserved | | unsigned dword | |
|
||||
| 0008h-0009h | export energy | 1/100 kWh | unsigned dword | R<> |
|
||||
| 000Ah-000Bh | import energy | 1/100 kWh | unsigned dword | R<> |
|
||||
| 000Ch | voltage | 1/10 V | unsigned word | R |
|
||||
| 000Dh | current | 1/100 A | unsigned word | R |
|
||||
| 000Eh | active power | 1 W | signed word | R |
|
||||
| 000Fh | reactive power | 1 VAr | unsigned word | R |
|
||||
| 0010h | power factor | 1/1000 | unsigned word | R |
|
||||
| 0011h | frequency | 1/100 Hz | unsigned word | R |
|
||||
| 0012h | reserved | | unsigned word | |
|
||||
| 0013h | reserved | | unsigned word | |
|
||||
| 0014h | reserved | | unsigned word | |
|
||||
| 0015h:high | station address | 1-247 | unsigned char | R/W |
|
||||
| 0015h:low | baud rate | 1-4<> | unsigned char | R/W |
|
||||
| 001Ah | relay<61> | | unsigned word | R/W |
|
||||
|
||||
#### Notes:
|
||||
|
||||
##### Note 1:
|
||||
|
||||
Total, export and import energy counters can erased writing 0 in total energy
|
||||
registers.
|
||||
|
||||
##### Note 2:
|
||||
|
||||
Value mapping, default 1.
|
||||
|
||||
| Value | Baud rate |
|
||||
|:-----:|:---------:|
|
||||
| 1 | 9600 Bd |
|
||||
| 2 | 4800 Bd |
|
||||
| 3 | 2400 Bd |
|
||||
| 4 | 1200 Bd |
|
||||
|
||||
##### Note 3:
|
||||
|
||||
In DDS238-2 ZN/SR model the relay can be switched by 0x001A register.
|
||||
|
||||
| Value | Relay |
|
||||
|:-----:|:-----:|
|
||||
| 0 | Off |
|
||||
| 1 | On |
|
||||
|
||||
##### Data formats
|
||||
|
||||
| Data format | Lenght | Byte order |
|
||||
|-------------|--------:|------------|
|
||||
| char | 8 bits | |
|
||||
| word | 16 bits | Big endian |
|
||||
| dword | 32 bits | Big endian |
|
||||
|
||||
|
||||
### Writing registers
|
||||
|
||||
The meter does not understand the 'write sigle register' function code (06h),
|
||||
only the 'write multiple registers' function code (10h).
|
||||
|
||||
## Credits
|
||||
|
||||
Some part of the code has been taken from SolarEgde Modbus TCP Plugin. Thanks !
|
||||
https://github.com/addiejanssen/domoticz-solaredge-modbustcp-plugin/
|
||||
|
||||
|
||||
214
plugin.py
214
plugin.py
@ -7,7 +7,7 @@ Requirements:
|
||||
2. pymodbus AND pymodbusTCP
|
||||
"""
|
||||
"""
|
||||
<plugin key="DS238_ModbusTCP" name="DS238-2 D/ZN ModbusTCP" author="Xavier Beaudouin" version="0.0.1" externallink="https://github.com/xbeaudouin/domoticz-ds238-modbus-tcp">
|
||||
<plugin key="DS238_ModbusTCP" name="DS238-2 ZN/S ModbusTCP" author="Xavier Beaudouin" version="0.0.3" externallink="https://github.com/xbeaudouin/domoticz-ds238-modbus-tcp">
|
||||
<params>
|
||||
<param field="Address" label="IP Address" width="150px" required="true" />
|
||||
<param field="Port" label="Port Number" width="100px" required="true" default="502" />
|
||||
@ -30,6 +30,8 @@ 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
|
||||
|
||||
@ -37,9 +39,81 @@ 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)
|
||||
|
||||
#
|
||||
# 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:
|
||||
#enabled = False
|
||||
def __init__(self):
|
||||
# Voltage for last 5 minutes
|
||||
self.voltage=Average()
|
||||
# Current for last 5 minutes
|
||||
self.current=Average()
|
||||
# Active power for last 5 minutes
|
||||
self.active_power=Average()
|
||||
# Reactive power for last 5 minutes
|
||||
self.reactive_power=Average()
|
||||
# Power factor for last 5 minutes
|
||||
self.power_factor=Average()
|
||||
# Frequency for last 5 minutes
|
||||
self.frequency=Average()
|
||||
|
||||
return
|
||||
|
||||
def onStart(self):
|
||||
@ -71,36 +145,41 @@ class BasePlugin:
|
||||
Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on device : "+str(self.MBAddr))
|
||||
|
||||
# Create the devices if they does not exists
|
||||
# TODO: refactor this.
|
||||
if 1 not in Devices:
|
||||
Domoticz.Device(Name="Total Energy", Unit=1, Type=0x71, Subtype=0x0, Used=0).Create()
|
||||
# Voir 0x107 / 0x30 ?
|
||||
Domoticz.Device(Name="Total Energy", Unit=1, Type=0xfa, Subtype=0x01, Used=0).Create()
|
||||
if 2 not in Devices:
|
||||
Domoticz.Device(Name="Export Energy", Unit=2, Type=0x71, Subtype=0x0, Used=0).Create()
|
||||
Domoticz.Device(Name="Export Energy", Unit=2, Type=0xfa, Subtype=0x01, Used=0).Create()
|
||||
if 3 not in Devices:
|
||||
Domoticz.Device(Name="Import Energy", Unit=3, Type=0x71, Subtype=0x0, Used=0).Create()
|
||||
Domoticz.Device(Name="Import Energy", Unit=3, Type=0xfa, Subtype=0x01, Used=0).Create()
|
||||
if 4 not in Devices:
|
||||
Domoticz.Device(Name="Voltage", Unit=4, TypeName="Voltage", Used=0).Create()
|
||||
Domoticz.Device(Name="Voltage", Unit=4, TypeName="Voltage", Used=0).Create()
|
||||
if 5 not in Devices:
|
||||
Domoticz.Device(Name="Current", Unit=5, TypeName="Current (Single)", Used=0).Create()
|
||||
Domoticz.Device(Name="Current", Unit=5, TypeName="Current (Single)", Used=0).Create()
|
||||
if 6 not in Devices:
|
||||
Options = { "Custom": "1;W" }
|
||||
Domoticz.Device(Name="Active Power", Unit=6, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
Domoticz.Device(Name="Active Power", Unit=6, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
if 7 not in Devices:
|
||||
Options = { "Custom": "1;VAr" }
|
||||
Domoticz.Device(Name="Reactive Power", Unit=7, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
Domoticz.Device(Name="Reactive Power", Unit=7, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
if 8 not in Devices:
|
||||
Options = { "Custom": "1;PF" }
|
||||
Domoticz.Device(Name="Power Factor", Unit=8, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
Domoticz.Device(Name="Power Factor", Unit=8, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
if 9 not in Devices:
|
||||
Options = { "Custom": "1;Hz" }
|
||||
Domoticz.Device(Name="Frequency", Unit=9, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
Domoticz.Device(Name="Frequency", Unit=9, TypeName="Custom", Used=0, Options=Options).Create()
|
||||
if 10 not in Devices:
|
||||
Domoticz.Device(Name="Total Power Meter",Unit=10,Type=0xfa, Subtype=0x01, Used=0).Create()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def onStop(self):
|
||||
Domoticz.Log("onStop called")
|
||||
Domoticz.Debugging(0)
|
||||
|
||||
def onConnect(self, Connection, Status, Description):
|
||||
Domoticz.Log("onConnect called")
|
||||
return
|
||||
|
||||
def onMessage(self, Connection, Data):
|
||||
Domoticz.Log("onMessage called")
|
||||
@ -130,6 +209,72 @@ class BasePlugin:
|
||||
Devices[7].Update(1, "0")
|
||||
Devices[8].Update(1, "0")
|
||||
Devices[9].Update(1, "0")
|
||||
Devices[10].Update(1, "0")
|
||||
|
||||
# 3 counters
|
||||
total_e = "0"
|
||||
export_e = "0"
|
||||
import_e = "0"
|
||||
export_w = 0
|
||||
import_w = 0
|
||||
power = "0"
|
||||
|
||||
# Total Energy
|
||||
total_e = str(getmodbus32(0, client))
|
||||
|
||||
# Export Energy
|
||||
export_e = str(getmodbus32(0x8, client))
|
||||
|
||||
# Import Energy
|
||||
import_e = str(getmodbus32(0xA, client))
|
||||
|
||||
# Voltage
|
||||
value = round (getmodbus16(0xC,client) / 10, 3)
|
||||
self.voltage.update(value)
|
||||
value = self.voltage.get()
|
||||
Devices[4].Update(1, str(value))
|
||||
|
||||
# Current
|
||||
value = round (getmodbus16(0xD,client) / 100, 3)
|
||||
self.current.update(value)
|
||||
value = self.current.get()
|
||||
Devices[5].Update(1, str(value))
|
||||
|
||||
# Active Power
|
||||
value = getmodbus16(0xE, client)
|
||||
self.active_power.update(value)
|
||||
value = self.active_power.get()
|
||||
Devices[6].Update(1, str(value))
|
||||
if value > 0.0:
|
||||
import_w = value
|
||||
if value < 0.0:
|
||||
export_w = value
|
||||
power = str(abs(value))
|
||||
|
||||
# Reactive Power
|
||||
value = getmodbus16(0xF, client)
|
||||
self.reactive_power.update(value)
|
||||
value = self.reactive_power.get()
|
||||
Devices[7].Update(1, str(value))
|
||||
|
||||
# Power Factor
|
||||
value = round (getmodbus16(0x10,client) / 1000, 3)
|
||||
self.power_factor.update(value)
|
||||
value = self.power_factor.get()
|
||||
Devices[8].Update(1, str(value))
|
||||
|
||||
# Frequency
|
||||
value = round (getmodbus16(0x11, client) / 100, 3)
|
||||
self.frequency.update(value)
|
||||
value = self.frequency.get()
|
||||
Devices[9].Update(1, str(value))
|
||||
|
||||
# Do insert data on counters
|
||||
Devices[1].Update(1, sValue=total_e+"0;0;0;0;"+power+";0")
|
||||
Devices[2].Update(1, sValue=export_e+"0;0;0;0;"+str(abs(export_w))+";0")
|
||||
Devices[3].Update(1, sValue=import_e+"0;0;0;0;"+str(abs(import_w))+";0")
|
||||
Devices[10].Update(1, sValue=import_e+"0;0;"+export_e+"0;0;"+str(abs(import_w))+";"+str(abs(export_w)))
|
||||
|
||||
|
||||
global _plugin
|
||||
_plugin = BasePlugin()
|
||||
@ -180,3 +325,48 @@ def DumpConfigToLog():
|
||||
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
|
||||
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
|
||||
return
|
||||
|
||||
# get Modbus 32 bits values
|
||||
def getmodbus32(register, client):
|
||||
value = 0
|
||||
try:
|
||||
data = client.read_holding_registers(register, 2)
|
||||
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_32bit_int()
|
||||
except:
|
||||
Domoticz.Error("Error getting data from "+str(register) + ", try 1")
|
||||
try:
|
||||
data = client.read_holding_registers(register, 2)
|
||||
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_32bit_int()
|
||||
except:
|
||||
Domoticz.Error("Error getting data from "+str(register) + ", try 2")
|
||||
|
||||
return value
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
BIN
screenshots/Devices.png
Normal file
BIN
screenshots/Devices.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
BIN
screenshots/Hardware.png
Normal file
BIN
screenshots/Hardware.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
BIN
screenshots/TotalPower.png
Normal file
BIN
screenshots/TotalPower.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
BIN
screenshots/Voltage.png
Normal file
BIN
screenshots/Voltage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Reference in New Issue
Block a user