diff --git a/API_changes.rst b/API_changes.rst index 834127a4f..92f70ac8a 100644 --- a/API_changes.rst +++ b/API_changes.rst @@ -5,6 +5,7 @@ PyModbus - API changes. ------------- Version 3.3.0 ------------- +- ModbusTcpDiagClient is removed due to lack of support - Clients have an optional parameter: on_reconnect_callback, Function that will be called just before a reconnection attempt. - general parameter unit= -> slave= - move SqlSlaveContext, RedisSlaveContext to examples/contrib (due to lack of maintenance) diff --git a/pymodbus/client/sync_diag.py b/pymodbus/client/sync_diag.py deleted file mode 100644 index 63ad2a890..000000000 --- a/pymodbus/client/sync_diag.py +++ /dev/null @@ -1,168 +0,0 @@ -"""Sync diag.""" - -__all__ = [ - "ModbusTcpDiagClient", - "get_client", -] - -import socket -import time - -from pymodbus.client.tcp import ModbusTcpClient -from pymodbus.constants import Defaults -from pymodbus.exceptions import ConnectionException -from pymodbus.framer.socket_framer import ModbusSocketFramer -from pymodbus.logging import Log - - -LOG_MSGS = { - "conn_msg": "Connecting to modbus device %s", - "connfail_msg": "Connection to (%s, %s) failed: %s", - "discon_msg": "Disconnecting from modbus device %s", - "timelimit_read_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes in timelimit read", - "timeout_msg": "Modbus device timeout after %.4f seconds, returned %s bytes %s", - "delay_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes of %s expected", - "read_msg": "Modbus device read took %.4f seconds, " - "returned %s bytes of %s expected", - "unexpected_dc_msg": "%s %s", -} - - -class ModbusTcpDiagClient(ModbusTcpClient): - """Variant of pymodbus.client.ModbusTcpClient. - - With additional logging to diagnose network issues. - - The following events are logged: - - +---------+-----------------------------------------------------------------+ - | Level | Events | - +=========+=================================================================+ - | ERROR | Failure to connect to modbus slave; unexpected disconnect by | - | | modbus slave | - +---------+-----------------------------------------------------------------+ - | WARNING | Timeout on normal read; read took longer than warn_delay_limit | - +---------+-----------------------------------------------------------------+ - | INFO | Connection attempt to modbus slave; disconnection from modbus | - | | slave; each time limited read | - +---------+-----------------------------------------------------------------+ - | DEBUG | Normal read with timing information | - +---------+-----------------------------------------------------------------+ - - Reads are differentiated between "normal", which reads a specified number of - bytes, and "time limited", which reads all data for a duration equal to the - timeout period configured for this instance. - """ - - def __init__( - self, - host="127.0.0.1", - port=Defaults.TcpPort, - framer=ModbusSocketFramer, - **kwargs, - ): - """Initialize a client instance. - - The keys of LOG_MSGS can be used in kwargs to customize the messages. - - :param host: The host to connect to (default 127.0.0.1) - :param port: The modbus port to connect to (default 502) - :param source_address: The source address tuple to bind to (default ("", 0)) - :param timeout: The timeout to use for this socket (default Defaults.Timeout) - :param warn_delay_limit: Log reads that take longer than this as warning. - Default True sets it to half of "timeout". None never logs these as - warning, 0 logs everything as warning. - :param framer: The modbus framer to use (default ModbusSocketFramer) - - .. note:: The host argument will accept ipv4 and ipv6 hosts - """ - self.warn_delay_limit = kwargs.get("warn_delay_limit", True) - super().__init__(host, port, framer, **kwargs) - if self.warn_delay_limit is True: - self.warn_delay_limit = self.params.timeout / 2 - - # Set logging messages, defaulting to LOG_MSGS - for k_item, v_item in LOG_MSGS.items(): - self.__dict__[k_item] = kwargs.get(k_item, v_item) - - def connect(self): - """Connect to the modbus tcp server. - - :returns: True if connection succeeded, False otherwise - """ - if self.socket: - return True - try: - Log.info(LOG_MSGS["conn_msg"], self) - self.socket = socket.create_connection( - (self.params.host, self.params.port), - timeout=self.params.timeout, - source_address=self.params.source_address, - ) - except OSError as msg: - Log.error(LOG_MSGS["connfail_msg"], self.params.host, self.params.port, msg) - self.close() - return self.socket is not None - - def close(self): - """Close the underlying socket connection.""" - if self.socket: - Log.info(LOG_MSGS["discon_msg"], self) - self.socket.close() - self.socket = None - - def recv(self, size): - """Receive data.""" - try: - start = time.time() - - result = super().recv(size) - - delay = time.time() - start - if self.warn_delay_limit is not None and delay >= self.warn_delay_limit: - self._log_delayed_response(len(result), size, delay) - elif not size: - Log.debug(LOG_MSGS["timelimit_read_msg"], delay, len(result)) - else: - Log.debug(LOG_MSGS["read_msg"], delay, len(result), size) - - return result - except ConnectionException as exc: - # Only log actual network errors, "if not self.socket" then it's a internal code issue - if "Connection unexpectedly closed" in exc.string: - Log.error(LOG_MSGS["unexpected_dc_msg"], self, exc) - raise ConnectionException from exc - - def _log_delayed_response(self, result_len, size, delay): - """Log delayed response.""" - if not size and result_len > 0: - Log.info(LOG_MSGS["timelimit_read_msg"], delay, result_len) - elif ( - (not result_len) or (size and result_len < size) - ) and delay >= self.params.timeout: - size_txt = size if size else "in timelimit read" - read_type = f"of {size_txt} expected" - Log.warning(LOG_MSGS["timeout_msg"], delay, result_len, read_type) - else: - Log.warning(LOG_MSGS["delay_msg"], delay, result_len, size) - - def __str__(self): - """Build a string representation of the connection. - - :returns: The string representation - """ - return f"ModbusTcpDiagClient({self.params.host}:{self.params.port})" - - -def get_client(): - """Return an appropriate client based on logging level. - - This will be ModbusTcpDiagClient by default, or the parent class - if the log level is such that the diagnostic client will not log - anything. - - :returns: ModbusTcpClient or a child class thereof - """ - return ModbusTcpDiagClient diff --git a/test/test_client_sync_diag.py b/test/test_client_sync_diag.py deleted file mode 100755 index 4f4c319dd..000000000 --- a/test/test_client_sync_diag.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Test client sync diag.""" -import socket -from itertools import count -from test.conftest import mockSocket -from unittest import mock - -import pytest - -from pymodbus.client.sync_diag import ModbusTcpDiagClient, get_client -from pymodbus.exceptions import ConnectionException - - -# ---------------------------------------------------------------------------# -# Fixture -# ---------------------------------------------------------------------------# - - -class TestSynchronousDiagnosticClient: - """Unittest for the pymodbus.client.sync_diag module. - - It is a copy of parts of the test for the TCP class in the pymodbus.client - module, as it should operate identically and only log some additional - lines. - """ - - # -----------------------------------------------------------------------# - # Test TCP Diagnostic Client - # -----------------------------------------------------------------------# - - def test_syn_tcp_diag_client_instantiation(self): - """Test sync tcp diag client.""" - client = get_client() - assert client - - def test_basic_syn_tcp_diag_client(self): - """Test the basic methods for the tcp sync diag client""" - # connect/disconnect - client = ModbusTcpDiagClient() - client.socket = mockSocket() - assert client.connect() - client.close() - - def test_tcp_diag_client_connect(self): - """Test the tcp sync diag client connection method""" - with mock.patch.object(socket, "create_connection") as mock_method: - mock_method.return_value = object() - client = ModbusTcpDiagClient() - assert client.connect() - - with mock.patch.object(socket, "create_connection") as mock_method: - mock_method.side_effect = OSError() - client = ModbusTcpDiagClient() - assert not client.connect() - - @mock.patch("pymodbus.client.tcp.time") - @mock.patch("pymodbus.client.sync_diag.time") - @mock.patch("pymodbus.client.tcp.select") - def test_tcp_diag_client_recv(self, mock_select, mock_diag_time, mock_time): - """Test the tcp sync diag client receive method""" - mock_select.select.return_value = [True] - mock_time.time.side_effect = count() - mock_diag_time.time.side_effect = count() - client = ModbusTcpDiagClient() - with pytest.raises(ConnectionException): - client.recv(1024) - client.socket = mockSocket() - # Test logging of non-delayed responses - client.socket.mock_prepare_receive(b"\x00") - assert b"\x00" in client.recv(None) - client.socket = mockSocket() - client.socket.mock_prepare_receive(b"\x00") - assert client.recv(1) == b"\x00" - - # Fool diagnostic logger into thinking we"re running late, - # test logging of delayed responses - mock_diag_time.time.side_effect = count(step=3) - client.socket.mock_prepare_receive(b"\x00" * 4) - assert client.recv(4) == b"\x00" * 4 - assert client.recv(0) == b"" - - client.socket.mock_prepare_receive(b"\x00\x01\x02") - client.timeout = 3 - assert client.recv(3) == b"\x00\x01\x02" - client.socket.mock_prepare_receive(b"\x00\x01\x02") - assert client.recv(2) == b"\x00\x01" - mock_select.select.return_value = [False] - assert client.recv(2) == b"" - client.socket = mockSocket() - client.socket.mock_prepare_receive(b"\x00") - mock_select.select.return_value = [True] - assert b"\x00" in client.recv(None) - - mock_socket = mock.MagicMock() - client.socket = mock_socket - mock_socket.recv.return_value = b"" - with pytest.raises(ConnectionException): - client.recv(1024) - client.socket = mockSocket() - client.socket.mock_prepare_receive(b"\x00\x01\x02") - assert client.recv(1024) == b"\x00\x01\x02" - - def test_tcp_diag_client_repr(self): - """Test tcp diag client.""" - client = ModbusTcpDiagClient() - rep = ( - f"<{client.__class__.__name__} at {hex(id(client))} " - f"socket={client.socket}, ipaddr={client.params.host}, " - f"port={client.params.port}, timeout={client.params.timeout}>" - ) - assert repr(client) == rep