Skip to content

Commit 38ec4d1

Browse files
authored
Custom scan batching (#15)
Expose modbus `scan_batching` setting as a configurable field in the YAML. This devices how many contiguous registers are scanned in one read operation. The default of 100 remains the same.
2 parents 1bc9e10 + d2fc071 commit 38ec4d1

7 files changed

Lines changed: 52 additions & 11 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ port: 502
3737
update_rate: 5
3838
address_offset: 0
3939
variant: sungrow
40+
scan_batching: 100
4041
```
4142
4243
`ip` (Required) The IP address of the modbus device to be polled. Presently only modbus TCP/IP is supported.
@@ -45,10 +46,12 @@ variant: sungrow
4546

4647
`update_rate` (Optional: default 5) The number of seconds between polls of the modbus device.
4748

48-
`address_offset` (Optional: default 0) This offset is applied to every register address to accomodate different Modbus addressing systems. In many Modbus devices the first register is enumerated as 1, other times 0. See section 4.4 of the Modbus spec.
49+
`address_offset` (Optional: default 0) This offset is applied to every register address to accommodate different Modbus addressing systems. In many Modbus devices the first register is enumerated as 1, other times 0. See section 4.4 of the Modbus spec.
4950

5051
`variant` (Optional) Allows variants of the ModbusTcpClient library to be used. Setting this to 'sungrow' enables support of SungrowModbusTcpClient. This library transparently decrypts the modbus comms with sungrow SH inverters running newer firmware versions.
5152

53+
`scan_batching` (Optional: default 100) Modbus read operations are more efficient in bigger batches of contiguous registers, but different devices have different limits on the size of the batched reads. This setting can also be helpful when building a modbus register map for an uncharted device. In some modbus devices a single invalid register in a read range will fail the entire read operation. By setting `scan_batching` to `1` each register will be scanned individually. This will be very inefficient and should not be used in production as it will saturate the link with many read operations.
54+
5255
```yaml
5356
registers:
5457
- pub_topic: "forced_charge/mode"

modbus4mqtt/Sungrow_SH5k_20.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ port: 502
33
update_rate: 5
44
address_offset: 0
55
variant: sungrow
6+
scan_batching: 100
67
registers:
78
- pub_topic: "no_export/partial/limit"
89
set_topic: "no_export/partial/limit/set"

modbus4mqtt/modbus4mqtt.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def connect_modbus(self):
3333
self._mb = modbus_interface.modbus_interface(self.config['ip'],
3434
self.config.get('port', 502),
3535
self.config.get('update_rate', 5),
36-
variant=self.config.get('variant', None))
36+
variant=self.config.get('variant', None),
37+
scan_batching=self.config.get('scan_batching', None))
3738
failed_attempts = 1
3839
while self._mb.connect():
3940
logging.warning("Modbus connection attempt {} failed. Retrying...".format(failed_attempts))

modbus4mqtt/modbus_interface.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
DEFAULT_SCAN_BATCHING = 100
1010
DEFAULT_WRITE_BLOCK_INTERVAL_S = 0.2
1111
DEFAULT_WRITE_SLEEP_S = 0.05
12+
DEFAULT_READ_SLEEP_S = 0.05
1213

1314
class modbus_interface():
1415

15-
def __init__(self, ip, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant=None):
16+
def __init__(self, ip, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant=None, scan_batching=None):
1617
self._ip = ip
1718
self._port = port
1819
# This is a dict of sets. Each key represents one table of modbus registers.
@@ -25,6 +26,10 @@ def __init__(self, ip, port=502, update_rate_s=DEFAULT_SCAN_RATE_S, variant=None
2526
self._planned_writes = Queue()
2627
self._writing = False
2728
self._variant = variant
29+
if scan_batching is None:
30+
self._scan_batching = DEFAULT_SCAN_BATCHING
31+
elif 1 <= scan_batching <= 100:
32+
self._scan_batching = scan_batching
2833

2934
def connect(self):
3035
# Connects to the modbus device
@@ -49,19 +54,21 @@ def add_monitor_register(self, table, addr):
4954
def poll(self):
5055
# Polls for the values marked as interesting in self._tables.
5156
for table in self._tables:
52-
# This batches up modbus reads in chunks of DEFAULT_SCAN_BATCHING
57+
# This batches up modbus reads in chunks of self._scan_batching
5358
start = -1
5459
for k in sorted(self._tables[table]):
55-
group = int(k) - int(k) % DEFAULT_SCAN_BATCHING
60+
group = int(k) - int(k) % self._scan_batching
5661
if (start < group):
5762
try:
58-
values = self._scan_value_range(table, group, DEFAULT_SCAN_BATCHING)
59-
for x in range(0, DEFAULT_SCAN_BATCHING):
63+
values = self._scan_value_range(table, group, self._scan_batching)
64+
for x in range(0, self._scan_batching):
6065
key = group + x
6166
self._values[table][key] = values[x]
67+
# Avoid back-to-back read operations that could overwhelm some modbus devices.
68+
sleep(DEFAULT_READ_SLEEP_S)
6269
except ValueError as e:
6370
logging.exception("{}".format(e))
64-
start = group + DEFAULT_SCAN_BATCHING-1
71+
start = group + self._scan_batching-1
6572
self._process_writes()
6673

6774
def get_value(self, table, addr):

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ pyyaml
33
click
44
paho-mqtt
55
pymodbus
6-
SungrowModbusTcpClient>=0.1.2
6+
SungrowModbusTcpClient>=0.1.5

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="modbus4mqtt",
8-
version="0.3.1",
8+
version="0.3.2",
99
author="Travis Howse",
1010
author_email="tjhowse@gmail.com",
1111
description="A YAML-defined bidirectional Modbus to MQTT interface",
@@ -18,7 +18,7 @@
1818
'paho-mqtt>=1.5.0',
1919
'pymodbus>=2.3.0',
2020
'click>=6.7',
21-
'SungrowModbusTcpClient>=0.1.2',
21+
'SungrowModbusTcpClient>=0.1.5',
2222
],
2323
tests_require=[
2424
'nose2>=0.9.2',

tests/test_modbus.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,32 @@ def test_masked_writes(self):
166166

167167
m.set_value('holding', 1, 0x0000, 0x0F00)
168168
self.assertEqual(self.holding_registers.registers[1], 0xF0FF)
169+
170+
def test_scan_batching_of_one(self):
171+
with patch('modbus4mqtt.modbus_interface.ModbusTcpClient') as mock_modbus:
172+
mock_modbus().connect.side_effect = self.connect_success
173+
mock_modbus().read_input_registers.side_effect = self.read_input_registers
174+
mock_modbus().read_holding_registers.side_effect = self.read_holding_registers
175+
176+
m = modbus_interface.modbus_interface('1.1.1.1', 111, 2, scan_batching=1)
177+
m.connect()
178+
mock_modbus.assert_called_with('1.1.1.1', 111, RetryOnEmpty=True, framer=modbus_interface.ModbusSocketFramer, retries=1, timeout=1)
179+
180+
# Confirm registers are added to the correct tables.
181+
m.add_monitor_register('holding', 5)
182+
m.add_monitor_register('holding', 6)
183+
m.add_monitor_register('input', 6)
184+
m.add_monitor_register('input', 7)
185+
186+
m.poll()
187+
188+
self.assertEqual(m.get_value('holding', 5), 5)
189+
self.assertEqual(m.get_value('holding', 6), 6)
190+
self.assertEqual(m.get_value('input', 6), 6)
191+
self.assertEqual(m.get_value('input', 7), 7)
192+
193+
# Ensure each register is scanned with a separate read call.
194+
mock_modbus().read_holding_registers.assert_any_call(5, 1, unit=1)
195+
mock_modbus().read_holding_registers.assert_any_call(6, 1, unit=1)
196+
mock_modbus().read_input_registers.assert_any_call(6, 1, unit=1)
197+
mock_modbus().read_input_registers.assert_any_call(7, 1, unit=1)

0 commit comments

Comments
 (0)