diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py index 80fa4b46c..e0772cf5c 100644 --- a/pymodbus/client/base.py +++ b/pymodbus/client/base.py @@ -4,7 +4,7 @@ import asyncio import socket from dataclasses import dataclass -from typing import Any, Callable, Type, cast +from typing import Any, Awaitable, Callable, Type, cast from pymodbus.client.mixin import ModbusClientMixin from pymodbus.exceptions import ConnectionException, ModbusIOException @@ -17,7 +17,7 @@ from pymodbus.utilities import ModbusTransactionState -class ModbusBaseClient(ModbusClientMixin, ModbusProtocol): +class ModbusBaseClient(ModbusClientMixin[Awaitable[ModbusResponse]], ModbusProtocol): """**ModbusBaseClient**. Fixed parameters: @@ -60,7 +60,7 @@ def __init__( **kwargs: Any, ) -> None: """Initialize a client instance.""" - ModbusClientMixin.__init__(self) + ModbusClientMixin.__init__(self) # type: ignore[arg-type] ModbusProtocol.__init__( self, CommParams( @@ -136,7 +136,7 @@ def idle_time(self) -> float: return 0 return self.last_frame_end + self.silent_interval - def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: + def execute(self, request: ModbusRequest | None = None): """Execute request and get response (call **sync/async**). :param request: The request to process @@ -150,7 +150,7 @@ def execute(self, request: ModbusRequest | None = None) -> ModbusResponse: # ----------------------------------------------------------------------- # # Merged client methods # ----------------------------------------------------------------------- # - async def async_execute(self, request=None): + async def async_execute(self, request=None) -> ModbusResponse: """Execute requests asynchronously.""" request.transaction_id = self.transaction.getNextTID() packet = self.framer.buildPacket(request) @@ -176,7 +176,7 @@ async def async_execute(self, request=None): f"ERROR: No response received after {self.retries} retries" ) - return resp + return resp # type: ignore[return-value] def callback_data(self, data: bytes, addr: tuple | None = None) -> int: """Handle received data. @@ -266,7 +266,8 @@ def __str__(self): f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}" ) -class ModbusBaseSyncClient(ModbusClientMixin, ModbusProtocol): + +class ModbusBaseSyncClient(ModbusClientMixin[ModbusResponse], ModbusProtocol): """**ModbusBaseClient**. Fixed parameters: @@ -319,7 +320,7 @@ def __init__( **kwargs: Any, ) -> None: """Initialize a client instance.""" - ModbusClientMixin.__init__(self) + ModbusClientMixin.__init__(self) # type: ignore[arg-type] ModbusProtocol.__init__( self, CommParams( @@ -337,7 +338,7 @@ def __init__( parity=kwargs.get("parity", None), stopbits=kwargs.get("stopbits", None), handle_local_echo=kwargs.get("handle_local_echo", False), - on_reconnect_callback = on_reconnect_callback, + on_reconnect_callback=on_reconnect_callback, ), False, ) diff --git a/pymodbus/client/mixin.py b/pymodbus/client/mixin.py index f119f6b20..f686761aa 100644 --- a/pymodbus/client/mixin.py +++ b/pymodbus/client/mixin.py @@ -2,9 +2,8 @@ from __future__ import annotations import struct -from collections.abc import Awaitable from enum import Enum -from typing import Any +from typing import Any, Generic, TypeVar import pymodbus.bit_read_message as pdu_bit_read import pymodbus.bit_write_message as pdu_bit_write @@ -15,10 +14,13 @@ import pymodbus.register_read_message as pdu_reg_read import pymodbus.register_write_message as pdu_req_write from pymodbus.exceptions import ModbusException -from pymodbus.pdu import ModbusRequest, ModbusResponse +from pymodbus.pdu import ModbusRequest -class ModbusClientMixin: # pylint: disable=too-many-public-methods +T = TypeVar("T", covariant=False) + + +class ModbusClientMixin(Generic[T]): # pylint: disable=too-many-public-methods """**ModbusClientMixin**. This is an interface class to facilitate the sending requests/receiving responses like read_coils. @@ -47,9 +49,7 @@ class ModbusClientMixin: # pylint: disable=too-many-public-methods def __init__(self): """Initialize.""" - def execute( - self, _request: ModbusRequest - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def execute(self, _request: ModbusRequest) -> T: """Execute request (code ???). :raises ModbusException: @@ -59,11 +59,13 @@ def execute( .. tip:: Response is not interpreted. """ - return ModbusResponse() + raise NotImplementedError( + "The execute method of ModbusClientMixin needs to be overridden and cannot be used directly" + ) def read_coils( self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read coils (code 0x01). :param address: Start address to read from @@ -78,7 +80,7 @@ def read_coils( def read_discrete_inputs( self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read discrete inputs (code 0x02). :param address: Start address to read from @@ -93,7 +95,7 @@ def read_discrete_inputs( def read_holding_registers( self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read holding registers (code 0x03). :param address: Start address to read from @@ -108,7 +110,7 @@ def read_holding_registers( def read_input_registers( self, address: int, count: int = 1, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read input registers (code 0x04). :param address: Start address to read from @@ -121,9 +123,7 @@ def read_input_registers( pdu_reg_read.ReadInputRegistersRequest(address, count, slave, **kwargs) ) - def write_coil( - self, address: int, value: bool, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def write_coil(self, address: int, value: bool, slave: int = 0, **kwargs: Any) -> T: """Write single coil (code 0x05). :param address: Address to write to @@ -138,7 +138,7 @@ def write_coil( def write_register( self, address: int, value: int, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Write register (code 0x06). :param address: Address to write to @@ -151,9 +151,7 @@ def write_register( pdu_req_write.WriteSingleRegisterRequest(address, value, slave, **kwargs) ) - def read_exception_status( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def read_exception_status(self, slave: int = 0, **kwargs: Any) -> T: """Read Exception Status (code 0x07). :param slave: (optional) Modbus slave ID @@ -162,9 +160,10 @@ def read_exception_status( """ return self.execute(pdu_other_msg.ReadExceptionStatusRequest(slave, **kwargs)) + def diag_query_data( self, msg: bytes, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Diagnose query data (code 0x08 sub 0x00). :param msg: Message to be returned @@ -176,7 +175,7 @@ def diag_query_data( def diag_restart_communication( self, toggle: bool, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Diagnose restart communication (code 0x08 sub 0x01). :param toggle: True if toggled. @@ -188,9 +187,7 @@ def diag_restart_communication( pdu_diag.RestartCommunicationsOptionRequest(toggle, slave=slave, **kwargs) ) - def diag_read_diagnostic_register( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_diagnostic_register(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read diagnostic register (code 0x08 sub 0x02). :param slave: (optional) Modbus slave ID @@ -201,9 +198,7 @@ def diag_read_diagnostic_register( pdu_diag.ReturnDiagnosticRegisterRequest(slave=slave, **kwargs) ) - def diag_change_ascii_input_delimeter( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_change_ascii_input_delimeter(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose change ASCII input delimiter (code 0x08 sub 0x03). :param slave: (optional) Modbus slave ID @@ -214,9 +209,7 @@ def diag_change_ascii_input_delimeter( pdu_diag.ChangeAsciiInputDelimiterRequest(slave=slave, **kwargs) ) - def diag_force_listen_only( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_force_listen_only(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose force listen only (code 0x08 sub 0x04). :param slave: (optional) Modbus slave ID @@ -225,9 +218,7 @@ def diag_force_listen_only( """ return self.execute(pdu_diag.ForceListenOnlyModeRequest(slave=slave, **kwargs)) - def diag_clear_counters( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_clear_counters(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose clear counters (code 0x08 sub 0x0A). :param slave: (optional) Modbus slave ID @@ -236,9 +227,7 @@ def diag_clear_counters( """ return self.execute(pdu_diag.ClearCountersRequest(slave=slave, **kwargs)) - def diag_read_bus_message_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_bus_message_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read bus message count (code 0x08 sub 0x0B). :param slave: (optional) Modbus slave ID @@ -249,9 +238,7 @@ def diag_read_bus_message_count( pdu_diag.ReturnBusMessageCountRequest(slave=slave, **kwargs) ) - def diag_read_bus_comm_error_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_bus_comm_error_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Bus Communication Error Count (code 0x08 sub 0x0C). :param slave: (optional) Modbus slave ID @@ -262,9 +249,7 @@ def diag_read_bus_comm_error_count( pdu_diag.ReturnBusCommunicationErrorCountRequest(slave=slave, **kwargs) ) - def diag_read_bus_exception_error_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_bus_exception_error_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Bus Exception Error Count (code 0x08 sub 0x0D). :param slave: (optional) Modbus slave ID @@ -275,9 +260,7 @@ def diag_read_bus_exception_error_count( pdu_diag.ReturnBusExceptionErrorCountRequest(slave=slave, **kwargs) ) - def diag_read_slave_message_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_slave_message_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Slave Message Count (code 0x08 sub 0x0E). :param slave: (optional) Modbus slave ID @@ -288,9 +271,7 @@ def diag_read_slave_message_count( pdu_diag.ReturnSlaveMessageCountRequest(slave=slave, **kwargs) ) - def diag_read_slave_no_response_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_slave_no_response_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Slave No Response Count (code 0x08 sub 0x0F). :param slave: (optional) Modbus slave ID @@ -301,9 +282,7 @@ def diag_read_slave_no_response_count( pdu_diag.ReturnSlaveNoResponseCountRequest(slave=slave, **kwargs) ) - def diag_read_slave_nak_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_slave_nak_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Slave NAK Count (code 0x08 sub 0x10). :param slave: (optional) Modbus slave ID @@ -312,9 +291,7 @@ def diag_read_slave_nak_count( """ return self.execute(pdu_diag.ReturnSlaveNAKCountRequest(slave=slave, **kwargs)) - def diag_read_slave_busy_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_slave_busy_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Slave Busy Count (code 0x08 sub 0x11). :param slave: (optional) Modbus slave ID @@ -323,9 +300,7 @@ def diag_read_slave_busy_count( """ return self.execute(pdu_diag.ReturnSlaveBusyCountRequest(slave=slave, **kwargs)) - def diag_read_bus_char_overrun_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_bus_char_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Bus Character Overrun Count (code 0x08 sub 0x12). :param slave: (optional) Modbus slave ID @@ -336,9 +311,7 @@ def diag_read_bus_char_overrun_count( pdu_diag.ReturnSlaveBusCharacterOverrunCountRequest(slave=slave, **kwargs) ) - def diag_read_iop_overrun_count( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_read_iop_overrun_count(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose read Iop overrun count (code 0x08 sub 0x13). :param slave: (optional) Modbus slave ID @@ -349,9 +322,7 @@ def diag_read_iop_overrun_count( pdu_diag.ReturnIopOverrunCountRequest(slave=slave, **kwargs) ) - def diag_clear_overrun_counter( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_clear_overrun_counter(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose Clear Overrun Counter and Flag (code 0x08 sub 0x14). :param slave: (optional) Modbus slave ID @@ -360,9 +331,7 @@ def diag_clear_overrun_counter( """ return self.execute(pdu_diag.ClearOverrunCountRequest(slave=slave, **kwargs)) - def diag_getclear_modbus_response( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_getclear_modbus_response(self, slave: int = 0, **kwargs: Any) -> T: """Diagnose Get/Clear modbus plus (code 0x08 sub 0x15). :param slave: (optional) Modbus slave ID @@ -371,9 +340,7 @@ def diag_getclear_modbus_response( """ return self.execute(pdu_diag.GetClearModbusPlusRequest(slave=slave, **kwargs)) - def diag_get_comm_event_counter( - self, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_get_comm_event_counter(self, **kwargs: Any) -> T: """Diagnose get event counter (code 0x0B). :param kwargs: (optional) Experimental parameters. @@ -381,9 +348,7 @@ def diag_get_comm_event_counter( """ return self.execute(pdu_other_msg.GetCommEventCounterRequest(**kwargs)) - def diag_get_comm_event_log( - self, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def diag_get_comm_event_log(self, **kwargs: Any) -> T: """Diagnose get event counter (code 0x0C). :param kwargs: (optional) Experimental parameters. @@ -397,7 +362,7 @@ def write_coils( values: list[bool] | bool, slave: int = 0, **kwargs: Any, - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Write coils (code 0x0F). :param address: Start address to write to @@ -412,7 +377,7 @@ def write_coils( def write_registers( self, address: int, values: list[int] | int, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Write registers (code 0x10). :param address: Start address to write to @@ -427,9 +392,7 @@ def write_registers( ) ) - def report_slave_id( - self, slave: int = 0, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def report_slave_id(self, slave: int = 0, **kwargs: Any) -> T: """Report slave ID (code 0x11). :param slave: (optional) Modbus slave ID @@ -438,9 +401,7 @@ def report_slave_id( """ return self.execute(pdu_other_msg.ReportSlaveIdRequest(slave, **kwargs)) - def read_file_record( - self, records: list[tuple], **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def read_file_record(self, records: list[tuple], **kwargs: Any) -> T: """Read file record (code 0x14). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -449,9 +410,7 @@ def read_file_record( """ return self.execute(pdu_file_msg.ReadFileRecordRequest(records, **kwargs)) - def write_file_record( - self, records: list[tuple], **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def write_file_record(self, records: list[tuple], **kwargs: Any) -> T: """Write file record (code 0x15). :param records: List of (Reference type, File number, Record Number, Record Length) @@ -466,7 +425,7 @@ def mask_write_register( and_mask: int = 0xFFFF, or_mask: int = 0x0000, **kwargs: Any, - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Mask write register (code 0x16). :param address: The mask pointer address (0x0000 to 0xffff) @@ -487,7 +446,7 @@ def readwrite_registers( values: list[int] | int = 0, slave: int = 0, **kwargs, - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read/Write registers (code 0x17). :param read_address: The address to start reading from @@ -509,9 +468,7 @@ def readwrite_registers( ) ) - def read_fifo_queue( - self, address: int = 0x0000, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + def read_fifo_queue(self, address: int = 0x0000, **kwargs: Any) -> T: """Read FIFO queue (code 0x18). :param address: The address to start reading from @@ -524,7 +481,7 @@ def read_fifo_queue( def read_device_information( self, read_code: int | None = None, object_id: int = 0x00, **kwargs: Any - ) -> ModbusResponse | Awaitable[ModbusResponse]: + ) -> T: """Read FIFO queue (code 0x2B sub 0x0E). :param read_code: The device information read code diff --git a/pymodbus/framer/base.py b/pymodbus/framer/base.py index d27eb354b..1c9b91f0a 100644 --- a/pymodbus/framer/base.py +++ b/pymodbus/framer/base.py @@ -142,3 +142,9 @@ def frameProcessIncomingPacket( self, _single, _callback, _slave, _tid=None, **kwargs ): """Process new packet pattern.""" + + def buildPacket(self, message): + """Create a ready to send modbus packet. + + :param message: The populated request/response to send + """ diff --git a/test/sub_client/test_client.py b/test/sub_client/test_client.py index 334c5357b..a75ba1bc7 100755 --- a/test/sub_client/test_client.py +++ b/test/sub_client/test_client.py @@ -20,6 +20,7 @@ from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.exceptions import ConnectionException, ModbusException, ModbusIOException +from pymodbus.pdu import ModbusRequest from pymodbus.transport import CommType @@ -597,6 +598,9 @@ async def test_client_build_response(): await client.build_response(0) async def test_client_mixin_execute(): - """Test dummy execute.""" + """Test dummy execute for both sync and async.""" client = ModbusClientMixin() - assert client.execute(None) + with pytest.raises(NotImplementedError): + client.execute(ModbusRequest()) + with pytest.raises(NotImplementedError): + await client.execute(ModbusRequest())