first commit

This commit is contained in:
2021-07-11 22:12:52 +02:00
commit 82c8406865
8 changed files with 597 additions and 0 deletions

23
LICENSE Normal file
View File

@ -0,0 +1,23 @@
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
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.

170
README.md Normal file
View File

@ -0,0 +1,170 @@
# Domoticz DS238-2 ZN/S ModbusTCP plugin
A Domoticz plugin to collect data from Hiking DS238-2 ZN/S power meter ModbusTCP.
## Requirements
You need the DS238 to be connected over modbus over TCP. Wired or wireless.
Setup that use this on this plugin is using PW21
You need also to find the address of the DS238. You can find it using the
push button. Per default the address is 1.
## Installation of the plugin
This guide is based on Ubuntu 20.04 LTS:
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/plugins
git clone https://github.com/xbeaudouin/domoticz-ds238-modbus-tcp.git
```
Go to the plugin folder and install all required addons:
``` shell
cd domoticz/plugins/domoticz-ds238-modbus-tcp
sudo pip3 install -r requirements.txt
```
Once that is done, restart domoticz:
``` shell
sudo service domoticz.sh restart
```
## Configuration in Domoticz
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 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.
This should result in a lot of new devices in the `Setup` -> `Devices` menu.
## Updating the plugin
Go to the plugin folder and get the new verion:
``` shell
cd domoticz/plugins/domoticz-ds238-modbus-tcp
git pull
```
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/

402
plugin.py Normal file
View File

@ -0,0 +1,402 @@
#!/usr/bin/env python
"""
Hiking DDS238-2 ZN/S Energy Meter
Author: Xavier Beaudouin
Requirements:
1. modbus over TCP adapter like PW21
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">
<params>
<param field="Address" label="IP Address" width="150px" required="true" />
<param field="Port" label="Port Number" width="100px" required="true" default="502" />
<param field="Mode3" label="Modbus address" width="100px" required="true" default="1" />
<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')
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)
#
# 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):
try:
Domoticz.Log("DS238-2 ZN/S Energy Meter TCP loaded!, using python v" + sys.version[:6] + " and pymodbus v" + pymodbus.__version__)
except:
Domoticz.Log("DS238-2 ZN/S Energy Meter TCP 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"])
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=0xfa, Subtype=0x01, Used=0).Create()
if 2 not in Devices:
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=0xfa, Subtype=0x01, Used=0).Create()
if 4 not in Devices:
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()
if 6 not in Devices:
Options = { "Custom": "1;W" }
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()
if 8 not in Devices:
Options = { "Custom": "1;PF" }
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()
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.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):
Domoticz.Debug(" Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr))
try:
client = 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[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")
# TODO: catch errors
# 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()
total_e = str(value)
# Export Energy
data = client.read_holding_registers(0x8, 2)
Domoticz.Debug("Data from register 0x8: "+str(data))
# Unsigned 32
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_32bit_int()
export_e = str(value)
# Import Energy
data = client.read_holding_registers(0xA, 2)
Domoticz.Debug("Data from register 0xA: "+str(data))
# Unsigned 32
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_32bit_int()
import_e = str(value)
# Voltage
data = client.read_holding_registers(0xC, 1)
Domoticz.Debug("Data from register 0xC: "+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, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.voltage.update(value)
value = self.voltage.get()
Domoticz.Debug(" = {}".format(value))
Devices[4].Update(1, str(value))
# Current
data = client.read_holding_registers(0xD, 1)
Domoticz.Debug("Data from register 0xD: "+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, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.current.update(value)
value = self.current.get()
Domoticz.Debug(" = {}".format(value))
Devices[5].Update(1, str(value))
# Active Power
data = client.read_holding_registers(0xE, 1)
Domoticz.Debug("Data from register 0xE: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.active_power.update(value)
value = self.active_power.get()
Domoticz.Debug(" = {}".format(value))
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 0xF: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.reactive_power.update(value)
value = self.reactive_power.get()
Domoticz.Debug(" = {}".format(value))
Devices[7].Update(1, str(value))
# Power Factor
data = client.read_holding_registers(0x10, 1)
Domoticz.Debug("Data from register 0x10: "+str(data))
# Unsigned 16
decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big)
# Value
value = decoder.decode_16bit_int()
# Scale factor / 1000
value = round (value / 1000, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.power_factor.update(value)
value = self.power_factor.get()
Domoticz.Debug(" = {}".format(value))
Devices[8].Update(1, str(value))
# Frequency
data = client.read_holding_registers(0x11, 1)
Domoticz.Debug("Data from register 0x11: "+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, 3)
Domoticz.Debug("Value after conversion : "+str(value))
Domoticz.Debug("-> Calculating average")
self.frequency.update(value)
value = self.frequency.get()
Domoticz.Debug(" = {}".format(value))
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()
def onStart():
global _plugin
_plugin.onStart()
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()
# 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

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pymodbus
pymodbusTCP

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