Skip to content

Commit ef46f90

Browse files
datastore: add async_setValues/getValues methods (#2165)
Co-authored-by: Ilkka Ollakka <ilkka.ollakka@cloudersolutions.com>
1 parent d8d9d70 commit ef46f90

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed

pymodbus/datastore/context.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""Context for datastore."""
2+
3+
from __future__ import annotations
4+
25
# pylint: disable=missing-type-doc
36
from pymodbus.datastore.store import ModbusSequentialDataBlock
47
from pymodbus.exceptions import NoSuchSlaveException
58
from pymodbus.logging import Log
69

710

8-
class ModbusBaseSlaveContext: # pylint: disable=too-few-public-methods
11+
class ModbusBaseSlaveContext:
912
"""Interface for a modbus slave data context.
1013
1114
Derived classes must implemented the following methods:
1215
reset(self)
1316
validate(self, fx, address, count=1)
14-
getValues(self, fx, address, count=1)
15-
setValues(self, fx, address, values)
17+
getValues/async_getValues(self, fc_as_hex, address, count=1)
18+
setValues/async_setValues(self, fc_as_hex, address, values)
1619
"""
1720

1821
_fx_mapper = {2: "d", 4: "i"}
@@ -27,6 +30,44 @@ def decode(self, fx):
2730
"""
2831
return self._fx_mapper[fx]
2932

33+
async def async_getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
34+
"""Get `count` values from datastore.
35+
36+
:param fc_as_hex: The function we are working with
37+
:param address: The starting address
38+
:param count: The number of values to retrieve
39+
:returns: The requested values from a:a+c
40+
"""
41+
return self.getValues(fc_as_hex, address, count)
42+
43+
async def async_setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
44+
"""Set the datastore with the supplied values.
45+
46+
:param fc_as_hex: The function we are working with
47+
:param address: The starting address
48+
:param values: The new values to be set
49+
"""
50+
self.setValues(fc_as_hex, address, values)
51+
52+
def getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
53+
"""Get `count` values from datastore.
54+
55+
:param fc_as_hex: The function we are working with
56+
:param address: The starting address
57+
:param count: The number of values to retrieve
58+
:returns: The requested values from a:a+c
59+
"""
60+
Log.error("getValues({},{},{}) not implemented!", fc_as_hex, address, count)
61+
return []
62+
63+
def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
64+
"""Set the datastore with the supplied values.
65+
66+
:param fc_as_hex: The function we are working with
67+
:param address: The starting address
68+
:param values: The new values to be set
69+
"""
70+
3071

3172
# ---------------------------------------------------------------------------#
3273
# Slave Contexts

pymodbus/datastore/store.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class BaseModbusDataBlock(ABC, Generic[V]):
7171
getValues(self, address, count=1)
7272
setValues(self, address, values)
7373
reset(self)
74+
75+
Derived classes can implemented the following async methods:
76+
async_getValues(self, address, count=1)
77+
async_setValues(self, address, values)
78+
but are not needed since these standard call the sync. methods.
7479
"""
7580

7681
values: V
@@ -86,6 +91,15 @@ def validate(self, address:int, count=1) -> bool:
8691
:raises TypeError:
8792
"""
8893

94+
async def async_getValues(self, address: int, count=1) -> Iterable:
95+
"""Return the requested values from the datastore.
96+
97+
:param address: The starting address
98+
:param count: The number of values to retrieve
99+
:raises TypeError:
100+
"""
101+
return self.getValues(address, count)
102+
89103
@abstractmethod
90104
def getValues(self, address:int, count=1) -> Iterable:
91105
"""Return the requested values from the datastore.
@@ -95,9 +109,18 @@ def getValues(self, address:int, count=1) -> Iterable:
95109
:raises TypeError:
96110
"""
97111

112+
async def async_setValues(self, address: int, values: list[int|bool]) -> None:
113+
"""Set the requested values in the datastore.
114+
115+
:param address: The starting address
116+
:param values: The values to store
117+
:raises TypeError:
118+
"""
119+
self.setValues(address, values)
120+
98121
@abstractmethod
99122
def setValues(self, address:int, values) -> None:
100-
"""Return the requested values from the datastore.
123+
"""Set the requested values in the datastore.
101124
102125
:param address: The starting address
103126
:param values: The values to store

test/test_remote_datastore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Test remote datastore."""
2+
23
from unittest import mock
34

45
import pytest
@@ -34,6 +35,18 @@ def test_remote_slave_set_values(self):
3435
# assert result.exception_code == 0x02
3536
# assert result.function_code == 0x90
3637

38+
async def test_remote_slave_async_set_values(self):
39+
"""Test setting values against a remote slave context."""
40+
client = mock.MagicMock()
41+
client.write_coils = mock.MagicMock(return_value=WriteMultipleCoilsResponse())
42+
client.write_registers = mock.MagicMock(
43+
return_value=ExceptionResponse(0x10, 0x02)
44+
)
45+
46+
context = RemoteSlaveContext(client)
47+
await context.async_setValues(0x0F, 0, [1])
48+
await context.async_setValues(0x10, 1, [1])
49+
3750
def test_remote_slave_get_values(self):
3851
"""Test getting values from a remote slave context."""
3952
client = mock.MagicMock()
@@ -54,6 +67,30 @@ def test_remote_slave_get_values(self):
5467
result = context.getValues(3, 0, 10)
5568
assert result != [10] * 10
5669

70+
async def test_remote_slave_async_get_values(self):
71+
"""Test getting values from a remote slave context."""
72+
client = mock.MagicMock()
73+
client.read_coils = mock.MagicMock(return_value=ReadCoilsResponse([1] * 10))
74+
client.read_input_registers = mock.MagicMock(
75+
return_value=ReadInputRegistersResponse([10] * 10)
76+
)
77+
client.read_holding_registers = mock.MagicMock(
78+
return_value=ExceptionResponse(0x15)
79+
)
80+
81+
context = RemoteSlaveContext(client)
82+
context.validate(1, 0, 10)
83+
result = await context.async_getValues(1, 0, 10)
84+
assert result == [1] * 10
85+
86+
context.validate(4, 0, 10)
87+
result = await context.async_getValues(4, 0, 10)
88+
assert result == [10] * 10
89+
90+
context.validate(3, 0, 10)
91+
result = await context.async_getValues(3, 0, 10)
92+
assert result != [10] * 10
93+
5794
def test_remote_slave_validate_values(self):
5895
"""Test validating against a remote slave context."""
5996
client = mock.MagicMock()

test/test_sparse_datastore.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
"""Test framers."""
22

3+
import pytest
34

45
from pymodbus.datastore import ModbusSparseDataBlock
56

67

8+
@pytest.mark.asyncio()
9+
async def test_check_async_sparsedatastore():
10+
"""Test check frame."""
11+
data_in_block = {
12+
1: 6720,
13+
2: 130,
14+
30: [0x0D, 0xFE],
15+
105: [1, 2, 3, 4],
16+
20000: [45, 241, 48],
17+
20008: 38,
18+
48140: [0x4208, 0xCCCD],
19+
}
20+
datablock = ModbusSparseDataBlock(data_in_block)
21+
for key, entry in data_in_block.items():
22+
if isinstance(entry, int):
23+
entry = [entry]
24+
for value in entry:
25+
assert datablock.validate(key, 1)
26+
assert await datablock.async_getValues(key, 1) == [value]
27+
key += 1
28+
29+
730
def test_check_sparsedatastore():
831
"""Test check frame."""
932
data_in_block = {

0 commit comments

Comments
 (0)