Skip to content

Commit

Permalink
modbus_tcp plugin: fix a named parameter and refactored with better e…
Browse files Browse the repository at this point in the history
…rror reporting
  • Loading branch information
bmxp committed Feb 23, 2025
1 parent 61b8ad4 commit f687805
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 48 deletions.
147 changes: 101 additions & 46 deletions modbus_tcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#########################################################################
# Copyright 2022 De Filippis Ivan
# Copyright 2022 Ronny Schulz
# Copyright 2023 Bernd Meiners
# Copyright 2023-2025 Bernd Meiners
#########################################################################
# This file is part of SmartHomeNG.
#
Expand Down Expand Up @@ -35,26 +35,35 @@
from pymodbus.payload import BinaryPayloadBuilder

from pymodbus.client.tcp import ModbusTcpClient
from pymodbus import ModbusException

import logging

AttrAddress = 'modBusAddress'
AttrType = 'modBusDataType'

AttrFactor = 'modBusFactor'
AttrByteOrder = 'modBusByteOrder'
AttrWordOrder = 'modBusWordOrder'
AttrSlaveUnit = 'modBusUnit'
AttrObjectType = 'modBusObjectType'
AttrDirection = 'modBusDirection'

BAD_VALUE_SINT16 = 0x8000
BAD_VALUE_SINT32 = 0x80000000

BAD_VALUE_UINT16 = 0xFFFF
BAD_VALUE_UINT32 = 0xFFFFFFFF
BAD_VALUE_UINT64 = 0xFFFFFFFFFFFFFFFF


class modbus_tcp(SmartPlugin):
"""
This class provides a Plugin for SmarthomeNG to read and or write to modbus
devices.
"""

PLUGIN_VERSION = '1.0.13'
PLUGIN_VERSION = '1.0.14'

def __init__(self, sh, *args, **kwargs):
"""
Expand Down Expand Up @@ -166,11 +175,7 @@ def parse_item(self, item):
if self.has_iattr(item.conf, AttrObjectType):
objectType = self.get_iattr_value(item.conf, AttrObjectType)

reg = str(objectType) # dictionary key: objectType.regAddr.slaveUnit // HoldingRegister.528.1
reg += '.'
reg += str(regAddr)
reg += '.'
reg += str(slaveUnit)
reg = self.makedictkey(objectType,regAddr,slaveUnit)

if self.has_iattr(item.conf, AttrDirection):
dataDirection = self.get_iattr_value(item.conf, AttrDirection)
Expand Down Expand Up @@ -227,14 +232,15 @@ def log_error(self, message):

def poll_device(self):
"""
Polls for updates of the device
This method is only needed, if the device (hardware/interface) does not propagate
changes on it's own, but has to be polled to get the actual status.
Poll data from modbus device
It is called by the scheduler which is set within run() method.
"""
if not self.alive:
return

if self.lock.locked():
self.log_error(f"poll_device already called an not ready for next poll")
return

with self.lock:
try:
Expand All @@ -256,37 +262,48 @@ def poll_device(self):
self.connected = False
return

startTime = datetime.now()
regCount = 0
try:
startTime = datetime.now()
regCount = 0

for reg, regPara in self._regToRead.items():
with self.lock:
value = self.__read_Registers(regPara)
try:
raw_value = self.__read_Registers(regPara)
# self.logger.debug(f"value read: {value} type: {type(value)}")
if value is not None:
item = regPara['item']
if regPara['factor'] != 1:
value = value * regPara['factor']
# self.logger.debug(f"value {value} multiply by: {regPara['factor']}")
item(value, self.get_fullname())
regCount += 1

if 'read_dt' in regPara:
regPara['last_read_dt'] = regPara['read_dt']

if 'value' in regPara:
regPara['last_value'] = regPara['value']

regPara['read_dt'] = datetime.now()
regPara['value'] = value
except ModbusException as e:
self.logger.error(f"ModbusException raised while reading: {e}")
break

if raw_value is None:
continue

if self.is_NaN( raw_value, regPara['dataType']):
self.logger.debug(f"value read: {raw_value} type: {type(value)} is a bad Value")
continue

value = raw_value
if regPara['factor'] != 1 and isinstance(value, (int, float)):
value *= regPara['factor']
# self.logger.debug(f"value {value} multiply by: {regPara['factor']}")

item = regPara['item']
item(value, self.get_fullname())
regCount += 1

if 'read_dt' in regPara:
regPara['last_read_dt'] = regPara['read_dt']

if 'value' in regPara:
regPara['last_value'] = regPara['value']

regPara['read_dt'] = datetime.now()
regPara['value'] = value

endTime = datetime.now()
duration = endTime - startTime
if regCount > 0:
self._pollStatus['last_dt'] = datetime.now()
self._pollStatus['regCount'] = regCount
self.logger.debug(f"poll_device: {regCount} register read required {duration} seconds")
except Exception as e:
self.logger.error(f"something went wrong in the poll_device function: {e}")

def update_item(self, item, caller=None, source=None, dest=None):
"""
Expand Down Expand Up @@ -340,11 +357,9 @@ def update_item(self, item, caller=None, source=None, dest=None):
# else:
# self.logger.debug(f'update_item:{item} default modBusObjectTyp: {objectType}')

reg = str(objectType) # Dict-key: HoldingRegister.528.1 *** objectType.regAddr.slaveUnit ***
reg += '.'
reg += str(regAddr)
reg += '.'
reg += str(slaveUnit)
# Dict-key construction: objectType.regAddr.slaveUnit e.g. HoldingRegister.528.1
reg = self.makedictkey(objectType,regAddr,slaveUnit)

if reg in self._regToWrite:
with self.lock:
regPara = self._regToWrite[reg]
Expand Down Expand Up @@ -374,6 +389,15 @@ def update_item(self, item, caller=None, source=None, dest=None):
self.logger.error(f"something went wrong in the __write_Registers function: {e}")

def __write_Registers(self, regPara, value):
"""Writes a given value to the register given in dict regPara
Args:
regPara (dict): key/value for object type, address, slaveUnit, datatype
value: the value to be written to the register
Returns:
_type_: _description_
"""
objectType = regPara['objectType']
address = regPara['regAddr']
slaveUnit = regPara['slaveUnit']
Expand Down Expand Up @@ -448,6 +472,7 @@ def __write_Registers(self, regPara, value):
return
else:
return

if result.isError():
self.logger.error(f"write error: {result} {objectType}.{address}.{slaveUnit} (address.slaveUnit)")
return None
Expand All @@ -467,10 +492,18 @@ def __write_Registers(self, regPara, value):
# regPara['write_dt'] = datetime.now()
# regPara['write_value'] = value

def __read_Registers(self, regPara):
def __read_Registers(self, regPara: dict):
"""Reads a register from modbus with parameters in passed dict
Args:
regPara (dict): key/value for object type, address, slaveUnit, datatype
Returns:
int/float/string: the read value
"""
objectType = regPara['objectType']
dataTypeStr = regPara['dataType']
dataType = ''.join(filter(str.isalpha, dataTypeStr))
dataType = ''.join(filter(str.isalpha, dataTypeStr)) # get the base type from eg. 'uint32' --> 'uint'
bo = regPara['byteOrder']
wo = regPara['wordOrder']
slaveUnit = regPara['slaveUnit']
Expand All @@ -479,7 +512,7 @@ def __read_Registers(self, regPara):
value = None

try:
bits = int(''.join(filter(str.isdigit, dataTypeStr)))
bits = int(''.join(filter(str.isdigit, dataTypeStr))) # get only bits from e.g. 'uint32' --> 32
except:
bits = 16

Expand All @@ -494,17 +527,18 @@ def __read_Registers(self, regPara):

# self.logger.debug(f"read {objectType}.{address}.{slaveUnit} (address.slaveUnit) regCount:{registerCount}")
if objectType == 'Coil':
result = self._Mclient.read_coils(address, registerCount, slave=slaveUnit)
result = self._Mclient.read_coils(address, count=registerCount, slave=slaveUnit)
elif objectType == 'DiscreteInput':
result = self._Mclient.read_discrete_inputs(address, registerCount, slave=slaveUnit)
result = self._Mclient.read_discrete_inputs(address, count=registerCount, slave=slaveUnit)
elif objectType == 'InputRegister':
result = self._Mclient.read_input_registers(address, registerCount, slave=slaveUnit)
result = self._Mclient.read_input_registers(address, count=registerCount, slave=slaveUnit)
elif objectType == 'HoldingRegister':
result = self._Mclient.read_holding_registers(address, registerCount, slave=slaveUnit)
result = self._Mclient.read_holding_registers(address, count=registerCount, slave=slaveUnit)
else:
self.logger.error(f"{AttrObjectType} not supported: {objectType}")
return

# https://pymodbus.readthedocs.io/en/latest/source/client.html#client-response-handling
if result.isError():
self.logger.error(f"read error: {result} {objectType}.{address}.{slaveUnit} (address.slaveUnit) regCount:{registerCount}")
return
Expand Down Expand Up @@ -558,3 +592,24 @@ def __read_Registers(self, regPara):
return decoder.decode_bits()
else:
self.logger.error(f"Number of bits or datatype not supported : {dataTypeStr}")

@staticmethod
def is_NaN( value, dataType: str) -> bool:
"""
Check if a returned value is a bad value and return True if it is
"""
if dataType == 'int16':
return value == BAD_VALUE_SINT16
elif dataType == 'int32':
return value == BAD_VALUE_SINT32
elif dataType == 'uint16':
return value == BAD_VALUE_UINT16
elif dataType == 'uint32':
return value == BAD_VALUE_UINT32
elif dataType == 'uint64':
return value == BAD_VALUE_UINT64

@staticmethod
def makedictkey(objectType: str, regAddr, slaveUnit) -> str:
# dictionary key: objectType.regAddr.slaveUnit // HoldingRegister.528.1
return f"{str(objectType)}.{str(regAddr)}.{str(slaveUnit)}"
4 changes: 2 additions & 2 deletions modbus_tcp/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ plugin:
keywords: modbus_tcp modbus smartmeter inverter heatpump
#documentation: http://smarthomeng.de/user/plugins/modbus_tcp/user_doc.html
support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1154368-einbindung-von-modbus-tcp
version: 1.0.13 # Plugin version
version: 1.0.14 # Plugin version
sh_minversion: '1.10' # minimum shNG version to use this plugin
#sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest)
py_minversion: '3.6'
py_minversion: '3.8'
# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest)
multi_instance: true # plugin supports multi instance
restartable: true
Expand Down

0 comments on commit f687805

Please sign in to comment.