Compare commits

...

14 Commits

Author SHA1 Message Date
30c785221e Fix for pymodbus 3.5.x 2023-09-23 16:28:04 +02:00
e0393a6353 Better way to make this working 2022-10-17 21:22:06 +02:00
ef88dd5f8c Remove "old" way to get data
Added 2 attempts to get data. Sometime the data is borken on decoding it,
so try to fetch again the data twice
2022-10-17 21:09:28 +02:00
fb5d47f25c Catch errors 2022-10-17 18:16:23 +02:00
3921bf4689 Credits. 2021-05-20 11:32:10 +02:00
9b40e23293 Added Screen shots 2021-05-20 11:12:22 +02:00
92bb2a27dc Added registers 2021-05-20 11:02:47 +02:00
00300e70e3 Fix README 2021-05-20 10:56:58 +02:00
eaf1087a0e Added Average() on :
- Voltage
- Current
- Active Power
- Reactive Power
- Power factor
- Frequency
2021-05-10 22:02:17 +02:00
f6ceba6605 Classes work but.... 2021-05-10 21:19:56 +02:00
24ab8a96e4 Tentative of average the power we got every 10 seconds. 2021-05-10 21:07:06 +02:00
250622a4f2 Update License 2021-05-08 17:26:58 +00:00
6442f4d1a8 Fixed counters, and Total power meter with "Return" Channel 2021-05-07 14:08:12 +02:00
63a697d4f4 Fix debug 2021-05-07 12:51:28 +02:00
7 changed files with 276 additions and 109 deletions

View File

@ -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.

102
README.md
View File

@ -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,14 +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 `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.
@ -77,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
![](screenshots/Hardware.png)
The devices page show all Domoticz devices that were created for it.
![](screenshots/Devices.png)
The voltage graphs.
![](screenshots/Voltage.png)
Total power meter with Return energy (usefull for PV/Wind turbine generators)
![](screenshots/TotalPower.png)
## 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/

279
plugin.py
View File

@ -7,7 +7,7 @@ Requirements:
2. pymodbus AND pymodbusTCP
"""
"""
<plugin key="DS238_ModbusTCP" name="DS238-2 ZN/S 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,37 +145,37 @@ 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()
Domoticz.Device(Name="Total Energy", Unit=1, Type=248, Subtype=33, Used=0).Create()
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=248, Subtype=33, 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=248, Subtype=33, 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")
@ -135,115 +209,71 @@ 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
data = client.read_holding_registers(0, 2)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 32
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_32bit_int()
# Scale factor / 100
value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[1].Update(1, value)
total_e = str(getmodbus32(0, client))
# Export Energy
data = client.read_holding_registers(0x8, 2)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 32
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_32bit_int()
# Scale factor / 100
value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[2].Update(1, value)
export_e = str(getmodbus32(0x8, client))
# Import Energy
data = client.read_holding_registers(0xA, 2)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 32
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_32bit_int()
# Scale factor / 100
value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[3].Update(1, value)
import_e = str(getmodbus32(0xA, client))
# Voltage
data = client.read_holding_registers(0xC, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 10
value = str ( round (value / 10, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[4].Update(1, value)
value = round (getmodbus16(0xC,client) / 10, 3)
self.voltage.update(value)
value = self.voltage.get()
Devices[4].Update(1, str(value))
# Current
data = client.read_holding_registers(0xD, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100
value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[5].Update(1, value)
value = round (getmodbus16(0xD,client) / 100, 3)
self.current.update(value)
value = self.current.get()
Devices[5].Update(1, str(value))
# Active Power
data = client.read_holding_registers(0xE, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100
#value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
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
data = client.read_holding_registers(0xF, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100
#value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
value = getmodbus16(0xF, client)
self.reactive_power.update(value)
value = self.reactive_power.get()
Devices[7].Update(1, str(value))
# Power Factor
data = client.read_holding_registers(0x10, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 1000
value = str ( round (value / 1000, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[8].Update(1, value)
value = round (getmodbus16(0x10,client) / 1000, 3)
self.power_factor.update(value)
value = self.power_factor.get()
Devices[8].Update(1, str(value))
# Frequency
data = client.read_holding_registers(0x11, 1)
Domoticz.Debug("Data from register 0: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 100
value = str ( round (value / 100, 3))
Domoticz.Debug("Value after conversion : "+str(value))
Devices[9].Update(1, value)
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
@ -295,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

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

BIN
screenshots/Hardware.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
screenshots/TotalPower.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

BIN
screenshots/Voltage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB