Skip to content

Commit

Permalink
datastore: add async_setValues/getValues methods (#2165)
Browse files Browse the repository at this point in the history
Co-authored-by: Ilkka Ollakka <ilkka.ollakka@cloudersolutions.com>
  • Loading branch information
janiversen and ilkka-ollakka committed Apr 15, 2024
1 parent e9c187e commit d1aed6f
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 4 deletions.
47 changes: 44 additions & 3 deletions pymodbus/datastore/context.py
@@ -1,18 +1,21 @@
"""Context for datastore."""

from __future__ import annotations

# pylint: disable=missing-type-doc
from pymodbus.datastore.store import ModbusSequentialDataBlock
from pymodbus.exceptions import NoSuchSlaveException
from pymodbus.logging import Log


class ModbusBaseSlaveContext: # pylint: disable=too-few-public-methods
class ModbusBaseSlaveContext:
"""Interface for a modbus slave data context.
Derived classes must implemented the following methods:
reset(self)
validate(self, fx, address, count=1)
getValues(self, fx, address, count=1)
setValues(self, fx, address, values)
getValues/async_getValues(self, fc_as_hex, address, count=1)
setValues/async_setValues(self, fc_as_hex, address, values)
"""

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

async def async_getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
"""Get `count` values from datastore.
:param fc_as_hex: The function we are working with
:param address: The starting address
:param count: The number of values to retrieve
:returns: The requested values from a:a+c
"""
return self.getValues(fc_as_hex, address, count)

async def async_setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
"""Set the datastore with the supplied values.
:param fc_as_hex: The function we are working with
:param address: The starting address
:param values: The new values to be set
"""
self.setValues(fc_as_hex, address, values)

def getValues(self, fc_as_hex: int, address: int, count: int = 1) -> list[int | bool | None]:
"""Get `count` values from datastore.
:param fc_as_hex: The function we are working with
:param address: The starting address
:param count: The number of values to retrieve
:returns: The requested values from a:a+c
"""
Log.error("getValues({},{},{}) not implemented!", fc_as_hex, address, count)
return []

def setValues(self, fc_as_hex: int, address: int, values: list[int | bool]) -> None:
"""Set the datastore with the supplied values.
:param fc_as_hex: The function we are working with
:param address: The starting address
:param values: The new values to be set
"""


# ---------------------------------------------------------------------------#
# Slave Contexts
Expand Down
25 changes: 24 additions & 1 deletion pymodbus/datastore/store.py
Expand Up @@ -72,6 +72,11 @@ class BaseModbusDataBlock(ABC, Generic[V]):
getValues(self, address, count=1)
setValues(self, address, values)
reset(self)
Derived classes can implemented the following async methods:
async_getValues(self, address, count=1)
async_setValues(self, address, values)
but are not needed since these standard call the sync. methods.
"""

values: V
Expand All @@ -87,6 +92,15 @@ def validate(self, address:int, count=1) -> bool:
:raises TypeError:
"""

async def async_getValues(self, address: int, count=1) -> Iterable:
"""Return the requested values from the datastore.
:param address: The starting address
:param count: The number of values to retrieve
:raises TypeError:
"""
return self.getValues(address, count)

@abstractmethod
def getValues(self, address:int, count=1) -> Iterable:
"""Return the requested values from the datastore.
Expand All @@ -96,9 +110,18 @@ def getValues(self, address:int, count=1) -> Iterable:
:raises TypeError:
"""

async def async_setValues(self, address: int, values: list[int|bool]) -> None:
"""Set the requested values in the datastore.
:param address: The starting address
:param values: The values to store
:raises TypeError:
"""
self.setValues(address, values)

@abstractmethod
def setValues(self, address:int, values) -> None:
"""Return the requested values from the datastore.
"""Set the requested values in the datastore.
:param address: The starting address
:param values: The values to store
Expand Down
37 changes: 37 additions & 0 deletions test/test_remote_datastore.py
@@ -1,4 +1,5 @@
"""Test remote datastore."""

from unittest import mock

import pytest
Expand Down Expand Up @@ -34,6 +35,18 @@ def test_remote_slave_set_values(self):
# assert result.exception_code == 0x02
# assert result.function_code == 0x90

async def test_remote_slave_async_set_values(self):
"""Test setting values against a remote slave context."""
client = mock.MagicMock()
client.write_coils = mock.MagicMock(return_value=WriteMultipleCoilsResponse())
client.write_registers = mock.MagicMock(
return_value=ExceptionResponse(0x10, 0x02)
)

context = RemoteSlaveContext(client)
await context.async_setValues(0x0F, 0, [1])
await context.async_setValues(0x10, 1, [1])

def test_remote_slave_get_values(self):
"""Test getting values from a remote slave context."""
client = mock.MagicMock()
Expand All @@ -54,6 +67,30 @@ def test_remote_slave_get_values(self):
result = context.getValues(3, 0, 10)
assert result != [10] * 10

async def test_remote_slave_async_get_values(self):
"""Test getting values from a remote slave context."""
client = mock.MagicMock()
client.read_coils = mock.MagicMock(return_value=ReadCoilsResponse([1] * 10))
client.read_input_registers = mock.MagicMock(
return_value=ReadInputRegistersResponse([10] * 10)
)
client.read_holding_registers = mock.MagicMock(
return_value=ExceptionResponse(0x15)
)

context = RemoteSlaveContext(client)
context.validate(1, 0, 10)
result = await context.async_getValues(1, 0, 10)
assert result == [1] * 10

context.validate(4, 0, 10)
result = await context.async_getValues(4, 0, 10)
assert result == [10] * 10

context.validate(3, 0, 10)
result = await context.async_getValues(3, 0, 10)
assert result != [10] * 10

def test_remote_slave_validate_values(self):
"""Test validating against a remote slave context."""
client = mock.MagicMock()
Expand Down
23 changes: 23 additions & 0 deletions test/test_sparse_datastore.py
@@ -1,9 +1,32 @@
"""Test framers."""

import pytest

from pymodbus.datastore import ModbusSparseDataBlock


@pytest.mark.asyncio()
async def test_check_async_sparsedatastore():
"""Test check frame."""
data_in_block = {
1: 6720,
2: 130,
30: [0x0D, 0xFE],
105: [1, 2, 3, 4],
20000: [45, 241, 48],
20008: 38,
48140: [0x4208, 0xCCCD],
}
datablock = ModbusSparseDataBlock(data_in_block)
for key, entry in data_in_block.items():
if isinstance(entry, int):
entry = [entry]
for value in entry:
assert datablock.validate(key, 1)
assert await datablock.async_getValues(key, 1) == [value]
key += 1


def test_check_sparsedatastore():
"""Test check frame."""
data_in_block = {
Expand Down

0 comments on commit d1aed6f

Please sign in to comment.