From 4d4558941a88190a6a96e4ca2e545566ced377d6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 2 Oct 2025 21:19:10 +0200 Subject: [PATCH] SimData and simDevice. --- doc/source/simulator/datamodel.rst | 17 ++ examples/server_datamodel.py | 18 +- pymodbus/constants.py | 68 +++++--- pymodbus/simulator/__init__.py | 8 +- pymodbus/simulator/simcore.py | 3 +- pymodbus/simulator/simdata.py | 266 +++++++---------------------- pymodbus/simulator/simdevice.py | 114 +++++++++++++ test/simulator/test_simdata.py | 22 +-- test/simulator/test_simdevice.py | 14 +- 9 files changed, 264 insertions(+), 266 deletions(-) create mode 100644 pymodbus/simulator/simdevice.py diff --git a/doc/source/simulator/datamodel.rst b/doc/source/simulator/datamodel.rst index e4a8039bc..a5cc787f6 100644 --- a/doc/source/simulator/datamodel.rst +++ b/doc/source/simulator/datamodel.rst @@ -39,3 +39,20 @@ Class definitions :undoc-members: :show-inheritance: :member-order: bysource + + +Action data class examples +^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +.. autoclass:: pymodbus.simulator.SimDataMinMax + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + +.. autoclass:: pymodbus.simulator.SimDataIncrement + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource diff --git a/examples/server_datamodel.py b/examples/server_datamodel.py index 4f4a8a412..4a7ce80ac 100755 --- a/examples/server_datamodel.py +++ b/examples/server_datamodel.py @@ -30,25 +30,21 @@ def define_datamodel(): """ # SimData can be instantiated with positional or optional parameters: assert SimData( - 5, 17, 10, DataType.REGISTERS + 5, 10, 17, DataType.REGISTERS ) == SimData( address=5, value=17, count=10, datatype=DataType.REGISTERS ) # Define a group of coils/direct inputs non-shared (address=15..31 each 1 bit) - block1 = SimData(address=15, value=True, count=16, datatype=DataType.BITS) + block1 = SimData(address=15, count=16, value=True, datatype=DataType.BITS) # Define a group of coils/direct inputs shared (address=15..31 each 16 bit) - block2 = SimData(address=15, value=0xFFFF, count=16, datatype=DataType.BITS) + block2 = SimData(address=15, count=16, value=0xFFFF, datatype=DataType.BITS) # Define a group of holding/input registers (remark NO difference between shared and non-shared) - block3 = SimData(10, 123.4, datatype=DataType.FLOAT32) - block4 = SimData(17, value=123, count=5, datatype=DataType.INT64) - block5 = SimData(27, "Hello ", datatype=DataType.STRING) - - # Please use DataType.DEFAULT to define register limits. - # this datatype only uses 1 object, whereas DataType.REGISTERS uses objects, - # mean DataType.DEFAULT is factors more efficient and much less memory consuming - # JAN TO CORRECT. + block3 = SimData(10, 1, 123.4, datatype=DataType.FLOAT32) + block4 = SimData(17, count=5, value=123, datatype=DataType.INT64) + block5 = SimData(27, 1, "Hello ", datatype=DataType.STRING) + block_def = SimData(0, count=1000, datatype=DataType.REGISTERS) # SimDevice can be instantiated with positional or optional parameters: diff --git a/pymodbus/constants.py b/pymodbus/constants.py index 18127e9cc..832fe31f3 100644 --- a/pymodbus/constants.py +++ b/pymodbus/constants.py @@ -4,6 +4,7 @@ values for the servers and clients. """ import enum +from typing import Union INTERNAL_ERROR = "Pymodbus internal error" @@ -120,43 +121,56 @@ class MoreData(enum.IntEnum): class DataType(enum.IntEnum): - """Register types, used to define of a group of registers. + """Register types, used to define of a group of registers. - This is the types pymodbus recognizes, actually the modbus standard do NOT define e.g. INT32, - but since nearly every device contain e.g. values of type INT32, it is available in pymodbus, - with automatic conversions to/from registers. - """ + This is the types pymodbus recognizes, actually the modbus standard do NOT define e.g. INT32, + but since nearly every device contain e.g. values of type INT32, it is available in pymodbus, + with automatic conversions to/from registers. + """ - #: 1 integer == 1 register - INT16 = enum.auto() + #: 1 integer == 1 register + INT16 = enum.auto() - #: 1 positive integer == 1 register - UINT16 = enum.auto() + #: 1 positive integer == 1 register + UINT16 = enum.auto() - #: 1 integer == 2 registers - INT32 = enum.auto() + #: 1 integer == 2 registers + INT32 = enum.auto() - #: 1 positive integer == 2 registers - UINT32 = enum.auto() + #: 1 positive integer == 2 registers + UINT32 = enum.auto() - #: 1 integer == 4 registers - INT64 = enum.auto() + #: 1 integer == 4 registers + INT64 = enum.auto() - #: 1 positive integer == 4 register - UINT64 = enum.auto() + #: 1 positive integer == 4 register + UINT64 = enum.auto() - #: 1 float == 2 registers - FLOAT32 = enum.auto() + #: 1 float == 2 registers + FLOAT32 = enum.auto() - #: 1 float == 4 registers - FLOAT64 = enum.auto() + #: 1 float == 4 registers + FLOAT64 = enum.auto() - #: 1 string == (len(string) / 2) registers - STRING = enum.auto() + #: 1 string == (len(string) / 2) registers + STRING = enum.auto() - #: 16 bits == 1 register - BITS = enum.auto() + #: 16 bits == 1 register + BITS = enum.auto() - #: Registers == 16bit - REGISTERS = enum.auto() + #: Registers == 2 bytes (identical to UINT16) + REGISTERS = enum.auto() +DATATYPE_STRUCT: dict[DataType, tuple[Union[type, tuple[type, ...]], int]] = { # pylint: disable=consider-using-namedtuple-or-dataclass + DataType.INT16: (int, 1), + DataType.UINT16: (int, 1), + DataType.INT32: (int, 2), + DataType.UINT32: (int, 2), + DataType.INT64: (int, 4), + DataType.UINT64: (int, 4), + DataType.FLOAT32: (float, 2), + DataType.FLOAT64: (float, 4), + DataType.STRING: (str, -1), + DataType.BITS: ((list, int, bool), -2), + DataType.REGISTERS: (int, 1), +} diff --git a/pymodbus/simulator/__init__.py b/pymodbus/simulator/__init__.py index c9f55b28b..26fe7ed12 100644 --- a/pymodbus/simulator/__init__.py +++ b/pymodbus/simulator/__init__.py @@ -1,17 +1,15 @@ """Simulator.""" __all__ = [ - "SimAction", "SimCore", "SimData", "SimDevice", "SimValueType", ] -from pymodbus.simulator.simcore import SimCore -from pymodbus.simulator.simdata import ( - SimAction, +from .simcore import SimCore +from .simdata import ( SimData, - SimDevice, SimValueType, ) +from .simdevice import SimDevice diff --git a/pymodbus/simulator/simcore.py b/pymodbus/simulator/simcore.py index 253c9574a..3f038f08f 100644 --- a/pymodbus/simulator/simcore.py +++ b/pymodbus/simulator/simcore.py @@ -1,7 +1,8 @@ """Simulator data model implementation.""" from __future__ import annotations -from .simdata import SimData, SimDevice +from .simdata import SimData +from .simdevice import SimDevice class SimCore: diff --git a/pymodbus/simulator/simdata.py b/pymodbus/simulator/simdata.py index f1c3d6465..b38e06c67 100644 --- a/pymodbus/simulator/simdata.py +++ b/pymodbus/simulator/simdata.py @@ -1,19 +1,22 @@ """Simulator data model classes.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import TypeAlias -from pymodbus.constants import DataType +from pymodbus.constants import DATATYPE_STRUCT, DataType +from pymodbus.pdu import ExceptionResponse -SimValueType: TypeAlias = int | float | str | bool | bytes -SimAction: TypeAlias = Callable[[SimValueType], SimValueType] | Awaitable[SimValueType] +SimValueTypeSimple: TypeAlias = int | float | str | bool | bytes +SimValueType: TypeAlias = SimValueTypeSimple | list[SimValueTypeSimple] +SimAction: TypeAlias = Callable[[int, int, list[int]], Awaitable[list[int] | ExceptionResponse]] @dataclass(frozen=True) class SimData: - """Configure a group of continuous identical registers. + """Configure a group of continuous identical values/registers. **Examples**: @@ -22,84 +25,67 @@ class SimData: SimData( address=100, count=5, - value=-123456 + value=12345678 + datatype=DataType.INT32 + ) + SimData( + address=100, + value=[1, 2, 3, 4, 5] datatype=DataType.INT32 ) - The above code defines 5 INT32, each with the value -123456, in total 10 registers (address 100-109) + Each SimData defines 5 INT32 in total 10 registers (address 100-109) .. code-block:: python SimData( address=100, count=17, - value=-True + value=True datatype=DataType.BITS ) - - The above code defines 17 BITS (coils), each with the value True. In non-shared mode addresses are 100-115. - - in shared mode BITS are stored in registers (16bit is one register), the address refer to the register, - addresses are 100-101 (with register 101 being padded with 15 bits) - - .. tip:: use DataType.DEFAULT to define register limits: - - .. code-block:: python - SimData( - address=0, # First legal registers - count=1000, # last legal register is r+count-1 - value=0x1234 # Default register value - datatype=DataType.DEFAULT + address=100, + value=[0xffff, 1] + datatype=DataType.BITS ) - The above code sets the range of legal registers to 0..999 all with the value 0x1234. - Accessing non-defined registers will cause an exception response. + Each SimData defines 17 BITS (coils), with value True. + + In block mode (CO and DI) addresses are 100-116 (each 1 bit) - Remark that DEFAULT can be overwritten with other definitions: + In shared mode BITS are stored in registers (16bit is 1 register), the address refer to the register, + addresses are 100-101 (with register 101 being padded with 15 bits set to False) .. code-block:: python SimData( - address=0, # First legal registers - count=1000, # last legal register is r+count-1 - value=0x1234 # Default register value - datatype=DataType.DEFAULT + address=0, + count=1000, + value=0x1234 + datatype=DataType.REGISTERS ) - SimData( - address=6, - count=1, - value=117 - datatype=DataType.INT32 - ) - - Is a legal and normal combination. - .. attention:: Using DataType.DEFAULT is a LOT more efficient to define all registers, than \ - the other datatypes. This is because default registers are not created unless written to, whereas \ - the registers of other datatypes are each created as objects. + Defines a range of addresses 0..999 each with the value 0x1234. """ - #: Address of first register, starting with 0. + #: Address of first register, starting with 0 (identical to the requests) address: int - #: Value of datatype, to initialize the registers (repeated with count, apart from string). + #: Count of datatype e.g. #: - #: Depending on in which block the object is used some value types are not legal e.g. float cannot - #: be used to define coils. - value: SimValueType = 0 - - #: Count of datatype e.g. count=3 datatype=DataType.INT32 is 6 registers. + #: - count=3 datatype=DataType.REGISTERS is 3 registers. + #: - count=3 datatype=DataType.INT32 is 6 registers. + #: - count=1 (default), value="ABCD" is 2 registers #: - #: DataType.STR is special, the value string is copied "count" times. - #: - #: - count=1, value="ABCD" is 2 registers - #: - count=3, value="ABCD" is 6 registers, with "ABCD" repeated 3 times. + #: Cannot be used if value is a list or datatype is DataType.STRING count: int = 1 - #: Datatype, used to check access and calculate register count. - #: - #: .. note:: Default is DataType.REGISTERS + #: Value/Values of datatype, + #: will automatically be converted to registers, according to datatype. + value: SimValueType = 0 + + #: Used to check access and convert value to/from registers. datatype: DataType = DataType.REGISTERS #: Optional function to call when registers are being read/written. @@ -108,170 +94,32 @@ class SimData: #: #: .. code-block:: python #: - #: def my_action( - #: addr: int, - #: value: SimValueType) -> SimValueType: - #: return value + 1 - #: #: async def my_action( - #: addr: int, - #: value: SimValueType) -> SimValueType: - #: return value + 1 + #: function_code: int, + #: address: int, + #: registers: list[int]) -> list[int] | ExceptionResponse: + #: + # return registers #: #: .. tip:: use functools.partial to add extra parameters if needed. action: SimAction | None = None - def __check_datatype(self): - """Check datatype.""" - if self.datatype == DataType.STRING and not isinstance(self.value, str): - raise TypeError("DataType.STRING but value not a string") - if self.datatype in ( - DataType.INT16, - DataType.UINT16, - DataType.INT32, - DataType.UINT32, - DataType.INT64, - DataType.UINT64, - ) and not isinstance(self.value, int): - raise TypeError("DataType.INT variant but value not a int") - if self.datatype in ( - DataType.FLOAT32, - DataType.FLOAT64, - ) and not isinstance(self.value, float): - raise TypeError("DataType.FLOAT variant but value not a float") - if self.datatype == DataType.BITS and not isinstance(self.value, (bool, int)): - raise TypeError("DataType.BITS but value not a bool or int") - if self.datatype == DataType.REGISTERS and not isinstance(self.value, int): - raise TypeError("DataType.REGISTERS but value not a int") - #if self.datatype == DataType.DEFAULT: - # if self.action: - # raise TypeError("DataType.DEFAULT cannot have an action") - # if not isinstance(self.value, int): - # raise TypeError("DataType.DEFAULT but value not a int") def __post_init__(self): """Define a group of registers.""" if not isinstance(self.address, int) or not 0 <= self.address < 65535: raise TypeError("0 <= address < 65535") - if not isinstance(self.count, int) or not 0 < self.count <= 65535: - raise TypeError("0 < count <= 65535") - if self.action and not callable(self.action): - raise TypeError("action not Callable or Awaitable (async)") + if not isinstance(self.count, int) or not 0 <= self.count < 65535: + raise TypeError("0 <= count < 65535") if not isinstance(self.datatype, DataType): - raise TypeError("datatype not DataType") - if not isinstance(self.value, SimValueType): - raise TypeError("value not a supported type") - self.__check_datatype() - -@dataclass(frozen=True) -class SimDevice: - """Configure a device with parameters and registers. - - Registers can be defined as shared or as 4 separate blocks. - - shared_block means all requests access the same registers, - allowing e.g. input registers to be read with read_holding_register. - - .. warning:: Shared mode cannot be mixed with non-shared mode ! - - In shared mode, individual coils/direct input cannot be addressed directly ! Instead - the register address is used with count. In non-shared mode coils/direct input can be - addressed directly individually. - - **Device with shared registers**:: - - SimDevice( - id=1, - block_shared=[SimData(...)] - ) - - **Device with non-shared registers**:: - - SimDevice( - id=1, - block_coil=[SimData(...)], - block_direct=[SimData(...)], - block_holding=[SimData(...)], - block_input=[SimData(...)], - ) - - A server can contain either a single :class:`SimDevice` or list of :class:`SimDevice` to simulate a - multipoint line. - """ - - #: Address of device - #: - #: Default 0 means accept all devices, except those specifically defined. - id: int = 0 - - #: Enforce type checking, if True access are controlled to be conform with datatypes. - #: - #: Used to control that e.g. INT32 are not read as INT16. - type_check: bool = False - - #: Use this block for shared registers (Modern devices). - #: - #: Requests accesses all registers in this block. - #: - #: .. warning:: cannot be used together with other block_* parameters! - block_shared: list[SimData] | None = None - - #: Use this block for non-shared registers (very old devices). - #: - #: In this block an address is a single coil, there are no registers. - #: - #: Request of type read/write_coil accesses this block. - #: - #: .. tip:: block_coil/direct/holding/input must all be defined - block_coil: list[SimData] | None = None - - #: Use this block for non-shared registers (very old devices). - #: - #: In this block an address is a single relay, there are no registers. - #: - #: Request of type read/write_direct_input accesses this block. - #: - #: .. tip:: block_coil/direct/holding/input must all be defined - block_direct: list[SimData] | None = None - - #: Use this block for non-shared registers (very old devices). - #: - #: In this block an address is a register. - #: - #: Request of type read/write_holding accesses this block. - #: - #: .. tip:: block_coil/direct/holding/input must all be defined - block_holding: list[SimData] | None = None - - #: Use this block for non-shared registers (very old devices). - #: - #: In this block an address is a register. - #: - #: Request of type read/write_input accesses this block. - #: - #: .. tip:: block_coil/direct/holding/input must all be defined - block_input: list[SimData] | None = None - - def __post_init__(self): - """Define a device.""" - if not isinstance(self.id, int) or not 0 <= self.id < 255: - raise TypeError("0 <= id < 255") - blocks = [(self.block_shared, "shared")] - if self.block_shared: - if self.block_coil or self.block_direct or self.block_holding or self.block_input: - raise TypeError("block_* cannot be combined with block_shared") - else: - blocks = [ - (self.block_coil, "coil"), - (self.block_direct, "direct"), - (self.block_holding, "holding"), - (self.block_input, "input")] - - for block, name in blocks: - if not block: - raise TypeError(f"block_{name} not defined") - if not isinstance(block, list): - raise TypeError(f"block_{name} not a list") - for entry in block: - if not isinstance(entry, SimData): - raise TypeError(f"block_{name} contains non SimData entries") + raise TypeError("datatype must by an DataType") + if isinstance(self.value, list): + if self.count > 1 or self.datatype == DataType.STRING: + raise TypeError("count > 1 cannot be combined with given values=") + for entry in self.value: + if not isinstance(entry, DATATYPE_STRUCT[self.datatype][0]) or isinstance(entry, str): + raise TypeError(f"elements in values must be {self.datatype!s} and not string") + elif not isinstance(self.value, DATATYPE_STRUCT[self.datatype][0]): + raise TypeError(f"value must be {self.datatype!s}") + if self.action and not (callable(self.action) and asyncio.iscoroutinefunction(self.action)): + raise TypeError("action not a async function") diff --git a/pymodbus/simulator/simdevice.py b/pymodbus/simulator/simdevice.py new file mode 100644 index 000000000..a1049e5ab --- /dev/null +++ b/pymodbus/simulator/simdevice.py @@ -0,0 +1,114 @@ +"""Simulator device model classes.""" +from __future__ import annotations + +from dataclasses import dataclass + +from .simdata import SimData + + +@dataclass(frozen=True) +class SimDevice: + """Configure a device with parameters and registers. + + Registers can be defined as shared or as 4 separate blocks. + + shared_block means all requests access the same registers, + e.g. read_input_register and read_holding_register + give the same result. + + .. warning:: Shared block cannot be mixed with non-shared blocks ! + + In shared mode, individual coils/direct input are not addressed directly ! + Instead the register address is used with count and each register contains 16 bit. + In non-shared mode coils/direct input can be addressed directly individually and + each register contain 1 bit. + + **Device with shared registers**:: + + SimDevice( + id=1, + block_shared=[SimData(...)] + ) + + **Device with non-shared registers**:: + + SimDevice( + id=1, + block_coil=[SimData(...)], + block_direct=[SimData(...)], + block_holding=[SimData(...)], + block_input=[SimData(...)], + ) + + A server can contain either a single :class:`SimDevice` or list of :class:`SimDevice` + to simulate a multipoint line. + """ + + #: Address of device + #: + #: Default 0 means accept all devices, except those specifically defined. + id: int = 0 + + #: Enforce type checking, if True access are controlled to be conform with datatypes. + #: + #: Used to control that e.g. INT32 are not read as INT16. + type_check: bool = False + + #: Use this block for shared registers (Modern devices). + #: + #: Requests accesses all registers in this block. + #: + #: .. warning:: cannot be used together with other block_* parameters! + block_shared: list[SimData] | None = None + + #: Use this block for devices which are divided in 4 blocks. + #: + #: In this block an address is a single coil, there are no registers. + #: + #: Request of type read/write_coil accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_coil: list[SimData] | None = None + + #: Use this block for devices which are divided in 4 blocks. + #: + #: In this block an address is a single relay, there are no registers. + #: + #: Request of type read/write_direct_input accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_direct: list[SimData] | None = None + + #: Use this block for devices which are divided in 4 blocks. + #: + #: In this block an address is a register. + #: + #: Request of type read/write_holding accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_holding: list[SimData] | None = None + + #: Use this block for non-shared registers (very old devices). + #: + #: In this block an address is a register. + #: + #: Request of type read/write_input accesses this block. + #: + #: .. tip:: block_coil/direct/holding/input must all be defined + block_input: list[SimData] | None = None + + def __post_init__(self): + """Define a device.""" + if not isinstance(self.id, int) or 255 < self.id < 0: + raise TypeError("0 <= id < 255") + blocks = (self.block_coil, self.block_direct, self.block_holding, self.block_input) + if self.block_shared: + if not isinstance(self.block_shared, list): + raise TypeError("block_shared must be a list") + for entry in blocks: + if entry: + raise TypeError(f"{entry} cannot be combined with block_shared") + return + for entry in blocks: + if not entry or not isinstance(entry, list): + raise TypeError(f"{entry} not defined or not a list") diff --git a/test/simulator/test_simdata.py b/test/simulator/test_simdata.py index 0b80d90da..87a22ce9d 100644 --- a/test/simulator/test_simdata.py +++ b/test/simulator/test_simdata.py @@ -1,5 +1,4 @@ """Test SimData.""" - import pytest from pymodbus.constants import DataType @@ -19,13 +18,15 @@ def test_simdata_address(self, address): with pytest.raises(TypeError): SimData(address) SimData(0) + SimData(2^16 -1) @pytest.mark.parametrize("count", ["not ok", 1.0, -1, 70000]) def test_simdata_count(self, count): """Test simdata count.""" with pytest.raises(TypeError): - SimData(address=0, count=count) - SimData(0, count=2) + SimData(0, count=count) + SimData(0, count=1) + SimData(0, count=2^16 -1) @pytest.mark.parametrize("datatype", ["not ok", 1.0, 11]) def test_simdata_datatype(self, datatype): @@ -39,22 +40,22 @@ def test_simdata_datatype(self, datatype): (1.0, DataType.FLOAT32), (11, DataType.REGISTERS), (True, DataType.BITS), - # (17, DataType.DEFAULT), + ([True, False], DataType.BITS), ]) def test_simdata_value_ok(self, value, value_type): """Test simdata value.""" SimData(0, value=value, datatype=value_type) @pytest.mark.parametrize(("value", "value_type"), [ - ([True, False], DataType.BITS), ({0: 1}, DataType.REGISTERS), ((1, 0), DataType.REGISTERS), (123, DataType.STRING), + (["ab", "cd"], DataType.STRING), ("", DataType.INT16), + ([123, ""], DataType.INT16), (123, DataType.FLOAT32), (123.0, DataType.BITS), (123.0, DataType.REGISTERS), - # ("", DataType.DEFAULT), ]) def test_simdata_value_not_ok(self, value, value_type): """Test simdata value.""" @@ -63,15 +64,14 @@ def test_simdata_value_not_ok(self, value, value_type): def test_simdata_action(self): """Test simdata action.""" - def dummy_action(): + async def async_dummy_action(): """Set action.""" - async def async_dummy_action(): + def dummy_action(): """Set action.""" with pytest.raises(TypeError): SimData(0, action="not_ok") - SimData(0, action=dummy_action) + with pytest.raises(TypeError): + SimData(0, action=dummy_action) SimData(0, action=async_dummy_action) - # with pytest.raises(TypeError): - # SimData(0, datatype=DataType.DEFAULT, action=dummy_action) diff --git a/test/simulator/test_simdevice.py b/test/simulator/test_simdevice.py index 21288404d..b3d89c8d6 100644 --- a/test/simulator/test_simdevice.py +++ b/test/simulator/test_simdevice.py @@ -15,17 +15,27 @@ def test_instanciate(self): @pytest.mark.parametrize("id", ["not ok", 1.0, 256]) def test_simid(self, id): - """Test that simdata can be objects.""" + """Test device id.""" with pytest.raises(TypeError): SimDevice(id=id) SimDevice(id=1, block_shared=[SimData(0)]) def test_block_shared(self): - """Test that simdata can be objects.""" + """Test shared block.""" with pytest.raises(TypeError): SimDevice(id=1, block_shared=[SimData(0)], block_coil=[SimData(0)]) + SimDevice(id=1, block_shared=[SimData(0)]) + + def test_block_non_shared(self): + """Test non-shared block.""" with pytest.raises(TypeError): SimDevice(id=1, block_coil=[SimData(0)]) + SimDevice(id=1, + block_coil=[SimData(0)], + block_direct=[SimData(0)], + block_holding=[SimData(0)], + block_input=[SimData(0)], + ) def test_wrong_block(self): """Test that simdata can be objects."""