We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Unreachable code in AsyncModbusTcpClient
### Versions

* Pymodbus: 3.0.0

### Pymodbus Specific
* Client: tcp - async

### Description

Unreachable code in else statement due to the early return in the try statement.
This causes that there is no logging message being reported and to never reset the delay if there would be due to a temporary disconnect.

See also `_connect` function of the async TCP client 
https://github.com/riptideio/pymodbus/blob/61d9e9ac20954634a453befae50576da2333e40b/pymodbus/client/tcp.py#L82-L100


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pymodbus/client/tcp.py`
Content:
```
1 """Modbus client async TCP communication."""
2 import asyncio
3 import logging
4 import select
5 import socket
6 import time
7 import typing
8 
9 from pymodbus.client.base import ModbusBaseClient, ModbusClientProtocol
10 from pymodbus.constants import Defaults
11 from pymodbus.exceptions import ConnectionException
12 from pymodbus.framer import ModbusFramer
13 from pymodbus.framer.socket_framer import ModbusSocketFramer
14 from pymodbus.utilities import ModbusTransactionState
15 
16 
17 _logger = logging.getLogger(__name__)
18 
19 
20 class AsyncModbusTcpClient(ModbusBaseClient):
21     """**AsyncModbusTcpClient**.
22 
23     :param host: Host IP address or host name
24     :param port: (optional) Port used for communication.
25     :param framer: (optional) Framer class.
26     :param source_address: (optional) source address of client,
27     :param kwargs: (optional) Experimental parameters
28 
29     Example::
30 
31         from pymodbus.client import AsyncModbusTcpClient
32 
33         async def run():
34             client = AsyncModbusTcpClient("localhost")
35 
36             await client.connect()
37             ...
38             await client.close()
39     """
40 
41     def __init__(
42         self,
43         host: str,
44         port: int = Defaults.TcpPort,
45         framer: ModbusFramer = ModbusSocketFramer,
46         source_address: typing.Tuple[str, int] = None,
47         **kwargs: any,
48     ) -> None:
49         """Initialize Asyncio Modbus TCP Client."""
50         self.protocol = None
51         super().__init__(framer=framer, **kwargs)
52         self.params.host = host
53         self.params.port = port
54         self.params.source_address = source_address
55         self.loop = None
56         self.connected = False
57         self.delay_ms = self.params.reconnect_delay
58 
59     async def connect(self):  # pylint: disable=invalid-overridden-method
60         """Initiate connection to start client."""
61         # force reconnect if required:
62         self.loop = asyncio.get_running_loop()
63 
64         txt = f"Connecting to {self.params.host}:{self.params.port}."
65         _logger.debug(txt)
66         return await self._connect()
67 
68     async def close(self):  # pylint: disable=invalid-overridden-method
69         """Stop client."""
70         if self.connected and self.protocol and self.protocol.transport:
71             self.protocol.transport.close()
72 
73         # prevent reconnect.
74         self.params.host = None
75 
76     def _create_protocol(self):
77         """Create initialized protocol instance with factory function."""
78         protocol = ModbusClientProtocol(framer=self.params.framer, **self.params.kwargs)
79         protocol.factory = self
80         return protocol
81 
82     async def _connect(self):
83         """Connect."""
84         _logger.debug("Connecting.")
85         try:
86             transport, protocol = await asyncio.wait_for(
87                 self.loop.create_connection(
88                     self._create_protocol, host=self.params.host, port=self.params.port
89                 ),
90                 timeout=self.params.timeout,
91             )
92             return transport, protocol
93         except Exception as exc:  # pylint: disable=broad-except
94             txt = f"Failed to connect: {exc}"
95             _logger.warning(txt)
96             asyncio.ensure_future(self._reconnect())
97         else:
98             txt = f"Connected to {self.params.host}:{self.params.port}."
99             _logger.info(txt)
100             self.reset_delay()
101 
102     def protocol_made_connection(self, protocol):
103         """Notify successful connection."""
104         _logger.info("Protocol made connection.")
105         if not self.connected:
106             self.connected = True
107             self.protocol = protocol
108         else:
109             _logger.error("Factory protocol connect callback called while connected.")
110 
111     def protocol_lost_connection(self, protocol):
112         """Notify lost connection."""
113         _logger.info("Protocol lost connection.")
114         if protocol is not self.protocol:
115             _logger.error(
116                 "Factory protocol callback called from unexpected protocol instance."
117             )
118 
119         self.connected = False
120         self.protocol = None
121         if self.params.host:
122             asyncio.ensure_future(self._reconnect())
123 
124     async def _reconnect(self):
125         """Reconnect."""
126         txt = f"Waiting {self.delay_ms} ms before next connection attempt."
127         _logger.debug(txt)
128         await asyncio.sleep(self.delay_ms / 1000)
129         self.delay_ms = 2 * self.delay_ms
130 
131         return await self._connect()
132 
133 
134 class ModbusTcpClient(ModbusBaseClient):
135     """**ModbusTcpClient**.
136 
137     :param host: Host IP address or host name
138     :param port: (optional) Port used for communication.
139     :param framer: (optional) Framer class.
140     :param source_address: (optional) source address of client,
141     :param kwargs: (optional) Experimental parameters
142 
143     Example::
144 
145         from pymodbus.client import ModbusTcpClient
146 
147         async def run():
148             client = ModbusTcpClient("localhost")
149 
150             client.connect()
151             ...
152             client.close()
153     """
154 
155     def __init__(
156         self,
157         host: str,
158         port: int = Defaults.TcpPort,
159         framer: ModbusFramer = ModbusSocketFramer,
160         source_address: typing.Tuple[str, int] = None,
161         **kwargs: any,
162     ) -> None:
163         """Initialize Modbus TCP Client."""
164         super().__init__(framer=framer, **kwargs)
165         self.params.host = host
166         self.params.port = port
167         self.params.source_address = source_address
168         self.socket = None
169 
170     @property
171     def connected(self):
172         """Connect internal."""
173         return self.connect()
174 
175     def connect(self):
176         """Connect to the modbus tcp server."""
177         if self.socket:
178             return True
179         try:
180             self.socket = socket.create_connection(
181                 (self.params.host, self.params.port),
182                 timeout=self.params.timeout,
183                 source_address=self.params.source_address,
184             )
185             txt = f"Connection to Modbus server established. Socket {self.socket.getsockname()}"
186             _logger.debug(txt)
187         except socket.error as msg:
188             txt = (
189                 f"Connection to ({self.params.host}, {self.params.port}) failed: {msg}"
190             )
191             _logger.error(txt)
192             self.close()
193         return self.socket is not None
194 
195     def close(self):
196         """Close the underlying socket connection."""
197         if self.socket:
198             self.socket.close()
199         self.socket = None
200 
201     def _check_read_buffer(self):
202         """Check read buffer."""
203         time_ = time.time()
204         end = time_ + self.params.timeout
205         data = None
206         ready = select.select([self.socket], [], [], end - time_)
207         if ready[0]:
208             data = self.socket.recv(1024)
209         return data
210 
211     def send(self, request):
212         """Send data on the underlying socket."""
213         super().send(request)
214         if not self.socket:
215             raise ConnectionException(str(self))
216         if self.state == ModbusTransactionState.RETRYING:
217             if data := self._check_read_buffer():
218                 return data
219 
220         if request:
221             return self.socket.send(request)
222         return 0
223 
224     def recv(self, size):
225         """Read data from the underlying descriptor."""
226         super().recv(size)
227         if not self.socket:
228             raise ConnectionException(str(self))
229 
230         # socket.recv(size) waits until it gets some data from the host but
231         # not necessarily the entire response that can be fragmented in
232         # many packets.
233         # To avoid split responses to be recognized as invalid
234         # messages and to be discarded, loops socket.recv until full data
235         # is received or timeout is expired.
236         # If timeout expires returns the read data, also if its length is
237         # less than the expected size.
238         self.socket.setblocking(0)
239 
240         timeout = self.params.timeout
241 
242         # If size isn"t specified read up to 4096 bytes at a time.
243         if size is None:
244             recv_size = 4096
245         else:
246             recv_size = size
247 
248         data = []
249         data_length = 0
250         time_ = time.time()
251         end = time_ + timeout
252         while recv_size > 0:
253             try:
254                 ready = select.select([self.socket], [], [], end - time_)
255             except ValueError:
256                 return self._handle_abrupt_socket_close(size, data, time.time() - time_)
257             if ready[0]:
258                 if (recv_data := self.socket.recv(recv_size)) == b"":
259                     return self._handle_abrupt_socket_close(
260                         size, data, time.time() - time_
261                     )
262                 data.append(recv_data)
263                 data_length += len(recv_data)
264             time_ = time.time()
265 
266             # If size isn"t specified continue to read until timeout expires.
267             if size:
268                 recv_size = size - data_length
269 
270             # Timeout is reduced also if some data has been received in order
271             # to avoid infinite loops when there isn"t an expected response
272             # size and the slave sends noisy data continuously.
273             if time_ > end:
274                 break
275 
276         return b"".join(data)
277 
278     def _handle_abrupt_socket_close(
279         self, size, data, duration
280     ):  # pylint: disable=missing-type-doc
281         """Handle unexpected socket close by remote end.
282 
283         Intended to be invoked after determining that the remote end
284         has unexpectedly closed the connection, to clean up and handle
285         the situation appropriately.
286 
287         :param size: The number of bytes that was attempted to read
288         :param data: The actual data returned
289         :param duration: Duration from the read was first attempted
290                until it was determined that the remote closed the
291                socket
292         :return: The more than zero bytes read from the remote end
293         :raises ConnectionException: If the remote end didn't send any
294                  data at all before closing the connection.
295         """
296         self.close()
297         size_txt = size if size else "unbounded read"
298         readsize = f"read of {size_txt} bytes"
299         msg = (
300             f"{self}: Connection unexpectedly closed "
301             f"{duration} seconds into {readsize}"
302         )
303         if data:
304             result = b"".join(data)
305             msg += f" after returning {len(result)} bytes"
306             _logger.warning(msg)
307             return result
308         msg += " without response from unit before it closed connection"
309         raise ConnectionException(msg)
310 
311     def is_socket_open(self):
312         """Check if socket is open."""
313         return self.socket is not None
314 
315     def __str__(self):
316         """Build a string representation of the connection.
317 
318         :returns: The string representation
319         """
320         return f"ModbusTcpClient({self.params.host}:{self.params.port})"
321 
322     def __repr__(self):
323         """Return string representation."""
324         return (
325             f"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, "
326             f"ipaddr={self.params.host}, port={self.params.port}, timeout={self.params.timeout}>"
327         )
328
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py
--- a/pymodbus/client/tcp.py
+++ b/pymodbus/client/tcp.py
@@ -89,7 +89,6 @@
                 ),
                 timeout=self.params.timeout,
             )
-            return transport, protocol
         except Exception as exc:  # pylint: disable=broad-except
             txt = f"Failed to connect: {exc}"
             _logger.warning(txt)
@@ -98,6 +97,7 @@
             txt = f"Connected to {self.params.host}:{self.params.port}."
             _logger.info(txt)
             self.reset_delay()
+            return transport, protocol
 
     def protocol_made_connection(self, protocol):
         """Notify successful connection."""

{"golden_diff": "diff --git a/pymodbus/client/tcp.py b/pymodbus/client/tcp.py\n--- a/pymodbus/client/tcp.py\n+++ b/pymodbus/client/tcp.py\n@@ -89,7 +89,6 @@\n                 ),\n                 timeout=self.params.timeout,\n             )\n-            return transport, protocol\n         except Exception as exc:  # pylint: disable=broad-except\n             txt = f\"Failed to connect: {exc}\"\n             _logger.warning(txt)\n@@ -98,6 +97,7 @@\n             txt = f\"Connected to {self.params.host}:{self.params.port}.\"\n             _logger.info(txt)\n             self.reset_delay()\n+            return transport, protocol\n \n     def protocol_made_connection(self, protocol):\n         \"\"\"Notify successful connection.\"\"\"\n", "issue": "Unreachable code in AsyncModbusTcpClient\n### Versions\r\n\r\n* Pymodbus: 3.0.0\r\n\r\n### Pymodbus Specific\r\n* Client: tcp - async\r\n\r\n### Description\r\n\r\nUnreachable code in else statement due to the early return in the try statement.\r\nThis causes that there is no logging message being reported and to never reset the delay if there would be due to a temporary disconnect.\r\n\r\nSee also `_connect` function of the async TCP client \r\nhttps://github.com/riptideio/pymodbus/blob/61d9e9ac20954634a453befae50576da2333e40b/pymodbus/client/tcp.py#L82-L100\r\n\n", "before_files": [{"content": "\"\"\"Modbus client async TCP communication.\"\"\"\nimport asyncio\nimport logging\nimport select\nimport socket\nimport time\nimport typing\n\nfrom pymodbus.client.base import ModbusBaseClient, ModbusClientProtocol\nfrom pymodbus.constants import Defaults\nfrom pymodbus.exceptions import ConnectionException\nfrom pymodbus.framer import ModbusFramer\nfrom pymodbus.framer.socket_framer import ModbusSocketFramer\nfrom pymodbus.utilities import ModbusTransactionState\n\n\n_logger = logging.getLogger(__name__)\n\n\nclass AsyncModbusTcpClient(ModbusBaseClient):\n    \"\"\"**AsyncModbusTcpClient**.\n\n    :param host: Host IP address or host name\n    :param port: (optional) Port used for communication.\n    :param framer: (optional) Framer class.\n    :param source_address: (optional) source address of client,\n    :param kwargs: (optional) Experimental parameters\n\n    Example::\n\n        from pymodbus.client import AsyncModbusTcpClient\n\n        async def run():\n            client = AsyncModbusTcpClient(\"localhost\")\n\n            await client.connect()\n            ...\n            await client.close()\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        port: int = Defaults.TcpPort,\n        framer: ModbusFramer = ModbusSocketFramer,\n        source_address: typing.Tuple[str, int] = None,\n        **kwargs: any,\n    ) -> None:\n        \"\"\"Initialize Asyncio Modbus TCP Client.\"\"\"\n        self.protocol = None\n        super().__init__(framer=framer, **kwargs)\n        self.params.host = host\n        self.params.port = port\n        self.params.source_address = source_address\n        self.loop = None\n        self.connected = False\n        self.delay_ms = self.params.reconnect_delay\n\n    async def connect(self):  # pylint: disable=invalid-overridden-method\n        \"\"\"Initiate connection to start client.\"\"\"\n        # force reconnect if required:\n        self.loop = asyncio.get_running_loop()\n\n        txt = f\"Connecting to {self.params.host}:{self.params.port}.\"\n        _logger.debug(txt)\n        return await self._connect()\n\n    async def close(self):  # pylint: disable=invalid-overridden-method\n        \"\"\"Stop client.\"\"\"\n        if self.connected and self.protocol and self.protocol.transport:\n            self.protocol.transport.close()\n\n        # prevent reconnect.\n        self.params.host = None\n\n    def _create_protocol(self):\n        \"\"\"Create initialized protocol instance with factory function.\"\"\"\n        protocol = ModbusClientProtocol(framer=self.params.framer, **self.params.kwargs)\n        protocol.factory = self\n        return protocol\n\n    async def _connect(self):\n        \"\"\"Connect.\"\"\"\n        _logger.debug(\"Connecting.\")\n        try:\n            transport, protocol = await asyncio.wait_for(\n                self.loop.create_connection(\n                    self._create_protocol, host=self.params.host, port=self.params.port\n                ),\n                timeout=self.params.timeout,\n            )\n            return transport, protocol\n        except Exception as exc:  # pylint: disable=broad-except\n            txt = f\"Failed to connect: {exc}\"\n            _logger.warning(txt)\n            asyncio.ensure_future(self._reconnect())\n        else:\n            txt = f\"Connected to {self.params.host}:{self.params.port}.\"\n            _logger.info(txt)\n            self.reset_delay()\n\n    def protocol_made_connection(self, protocol):\n        \"\"\"Notify successful connection.\"\"\"\n        _logger.info(\"Protocol made connection.\")\n        if not self.connected:\n            self.connected = True\n            self.protocol = protocol\n        else:\n            _logger.error(\"Factory protocol connect callback called while connected.\")\n\n    def protocol_lost_connection(self, protocol):\n        \"\"\"Notify lost connection.\"\"\"\n        _logger.info(\"Protocol lost connection.\")\n        if protocol is not self.protocol:\n            _logger.error(\n                \"Factory protocol callback called from unexpected protocol instance.\"\n            )\n\n        self.connected = False\n        self.protocol = None\n        if self.params.host:\n            asyncio.ensure_future(self._reconnect())\n\n    async def _reconnect(self):\n        \"\"\"Reconnect.\"\"\"\n        txt = f\"Waiting {self.delay_ms} ms before next connection attempt.\"\n        _logger.debug(txt)\n        await asyncio.sleep(self.delay_ms / 1000)\n        self.delay_ms = 2 * self.delay_ms\n\n        return await self._connect()\n\n\nclass ModbusTcpClient(ModbusBaseClient):\n    \"\"\"**ModbusTcpClient**.\n\n    :param host: Host IP address or host name\n    :param port: (optional) Port used for communication.\n    :param framer: (optional) Framer class.\n    :param source_address: (optional) source address of client,\n    :param kwargs: (optional) Experimental parameters\n\n    Example::\n\n        from pymodbus.client import ModbusTcpClient\n\n        async def run():\n            client = ModbusTcpClient(\"localhost\")\n\n            client.connect()\n            ...\n            client.close()\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        port: int = Defaults.TcpPort,\n        framer: ModbusFramer = ModbusSocketFramer,\n        source_address: typing.Tuple[str, int] = None,\n        **kwargs: any,\n    ) -> None:\n        \"\"\"Initialize Modbus TCP Client.\"\"\"\n        super().__init__(framer=framer, **kwargs)\n        self.params.host = host\n        self.params.port = port\n        self.params.source_address = source_address\n        self.socket = None\n\n    @property\n    def connected(self):\n        \"\"\"Connect internal.\"\"\"\n        return self.connect()\n\n    def connect(self):\n        \"\"\"Connect to the modbus tcp server.\"\"\"\n        if self.socket:\n            return True\n        try:\n            self.socket = socket.create_connection(\n                (self.params.host, self.params.port),\n                timeout=self.params.timeout,\n                source_address=self.params.source_address,\n            )\n            txt = f\"Connection to Modbus server established. Socket {self.socket.getsockname()}\"\n            _logger.debug(txt)\n        except socket.error as msg:\n            txt = (\n                f\"Connection to ({self.params.host}, {self.params.port}) failed: {msg}\"\n            )\n            _logger.error(txt)\n            self.close()\n        return self.socket is not None\n\n    def close(self):\n        \"\"\"Close the underlying socket connection.\"\"\"\n        if self.socket:\n            self.socket.close()\n        self.socket = None\n\n    def _check_read_buffer(self):\n        \"\"\"Check read buffer.\"\"\"\n        time_ = time.time()\n        end = time_ + self.params.timeout\n        data = None\n        ready = select.select([self.socket], [], [], end - time_)\n        if ready[0]:\n            data = self.socket.recv(1024)\n        return data\n\n    def send(self, request):\n        \"\"\"Send data on the underlying socket.\"\"\"\n        super().send(request)\n        if not self.socket:\n            raise ConnectionException(str(self))\n        if self.state == ModbusTransactionState.RETRYING:\n            if data := self._check_read_buffer():\n                return data\n\n        if request:\n            return self.socket.send(request)\n        return 0\n\n    def recv(self, size):\n        \"\"\"Read data from the underlying descriptor.\"\"\"\n        super().recv(size)\n        if not self.socket:\n            raise ConnectionException(str(self))\n\n        # socket.recv(size) waits until it gets some data from the host but\n        # not necessarily the entire response that can be fragmented in\n        # many packets.\n        # To avoid split responses to be recognized as invalid\n        # messages and to be discarded, loops socket.recv until full data\n        # is received or timeout is expired.\n        # If timeout expires returns the read data, also if its length is\n        # less than the expected size.\n        self.socket.setblocking(0)\n\n        timeout = self.params.timeout\n\n        # If size isn\"t specified read up to 4096 bytes at a time.\n        if size is None:\n            recv_size = 4096\n        else:\n            recv_size = size\n\n        data = []\n        data_length = 0\n        time_ = time.time()\n        end = time_ + timeout\n        while recv_size > 0:\n            try:\n                ready = select.select([self.socket], [], [], end - time_)\n            except ValueError:\n                return self._handle_abrupt_socket_close(size, data, time.time() - time_)\n            if ready[0]:\n                if (recv_data := self.socket.recv(recv_size)) == b\"\":\n                    return self._handle_abrupt_socket_close(\n                        size, data, time.time() - time_\n                    )\n                data.append(recv_data)\n                data_length += len(recv_data)\n            time_ = time.time()\n\n            # If size isn\"t specified continue to read until timeout expires.\n            if size:\n                recv_size = size - data_length\n\n            # Timeout is reduced also if some data has been received in order\n            # to avoid infinite loops when there isn\"t an expected response\n            # size and the slave sends noisy data continuously.\n            if time_ > end:\n                break\n\n        return b\"\".join(data)\n\n    def _handle_abrupt_socket_close(\n        self, size, data, duration\n    ):  # pylint: disable=missing-type-doc\n        \"\"\"Handle unexpected socket close by remote end.\n\n        Intended to be invoked after determining that the remote end\n        has unexpectedly closed the connection, to clean up and handle\n        the situation appropriately.\n\n        :param size: The number of bytes that was attempted to read\n        :param data: The actual data returned\n        :param duration: Duration from the read was first attempted\n               until it was determined that the remote closed the\n               socket\n        :return: The more than zero bytes read from the remote end\n        :raises ConnectionException: If the remote end didn't send any\n                 data at all before closing the connection.\n        \"\"\"\n        self.close()\n        size_txt = size if size else \"unbounded read\"\n        readsize = f\"read of {size_txt} bytes\"\n        msg = (\n            f\"{self}: Connection unexpectedly closed \"\n            f\"{duration} seconds into {readsize}\"\n        )\n        if data:\n            result = b\"\".join(data)\n            msg += f\" after returning {len(result)} bytes\"\n            _logger.warning(msg)\n            return result\n        msg += \" without response from unit before it closed connection\"\n        raise ConnectionException(msg)\n\n    def is_socket_open(self):\n        \"\"\"Check if socket is open.\"\"\"\n        return self.socket is not None\n\n    def __str__(self):\n        \"\"\"Build a string representation of the connection.\n\n        :returns: The string representation\n        \"\"\"\n        return f\"ModbusTcpClient({self.params.host}:{self.params.port})\"\n\n    def __repr__(self):\n        \"\"\"Return string representation.\"\"\"\n        return (\n            f\"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, \"\n            f\"ipaddr={self.params.host}, port={self.params.port}, timeout={self.params.timeout}>\"\n        )\n", "path": "pymodbus/client/tcp.py"}], "after_files": [{"content": "\"\"\"Modbus client async TCP communication.\"\"\"\nimport asyncio\nimport logging\nimport select\nimport socket\nimport time\nimport typing\n\nfrom pymodbus.client.base import ModbusBaseClient, ModbusClientProtocol\nfrom pymodbus.constants import Defaults\nfrom pymodbus.exceptions import ConnectionException\nfrom pymodbus.framer import ModbusFramer\nfrom pymodbus.framer.socket_framer import ModbusSocketFramer\nfrom pymodbus.utilities import ModbusTransactionState\n\n\n_logger = logging.getLogger(__name__)\n\n\nclass AsyncModbusTcpClient(ModbusBaseClient):\n    \"\"\"**AsyncModbusTcpClient**.\n\n    :param host: Host IP address or host name\n    :param port: (optional) Port used for communication.\n    :param framer: (optional) Framer class.\n    :param source_address: (optional) source address of client,\n    :param kwargs: (optional) Experimental parameters\n\n    Example::\n\n        from pymodbus.client import AsyncModbusTcpClient\n\n        async def run():\n            client = AsyncModbusTcpClient(\"localhost\")\n\n            await client.connect()\n            ...\n            await client.close()\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        port: int = Defaults.TcpPort,\n        framer: ModbusFramer = ModbusSocketFramer,\n        source_address: typing.Tuple[str, int] = None,\n        **kwargs: any,\n    ) -> None:\n        \"\"\"Initialize Asyncio Modbus TCP Client.\"\"\"\n        self.protocol = None\n        super().__init__(framer=framer, **kwargs)\n        self.params.host = host\n        self.params.port = port\n        self.params.source_address = source_address\n        self.loop = None\n        self.connected = False\n        self.delay_ms = self.params.reconnect_delay\n\n    async def connect(self):  # pylint: disable=invalid-overridden-method\n        \"\"\"Initiate connection to start client.\"\"\"\n        # force reconnect if required:\n        self.loop = asyncio.get_running_loop()\n\n        txt = f\"Connecting to {self.params.host}:{self.params.port}.\"\n        _logger.debug(txt)\n        return await self._connect()\n\n    async def close(self):  # pylint: disable=invalid-overridden-method\n        \"\"\"Stop client.\"\"\"\n        if self.connected and self.protocol and self.protocol.transport:\n            self.protocol.transport.close()\n\n        # prevent reconnect.\n        self.params.host = None\n\n    def _create_protocol(self):\n        \"\"\"Create initialized protocol instance with factory function.\"\"\"\n        protocol = ModbusClientProtocol(framer=self.params.framer, **self.params.kwargs)\n        protocol.factory = self\n        return protocol\n\n    async def _connect(self):\n        \"\"\"Connect.\"\"\"\n        _logger.debug(\"Connecting.\")\n        try:\n            transport, protocol = await asyncio.wait_for(\n                self.loop.create_connection(\n                    self._create_protocol, host=self.params.host, port=self.params.port\n                ),\n                timeout=self.params.timeout,\n            )\n        except Exception as exc:  # pylint: disable=broad-except\n            txt = f\"Failed to connect: {exc}\"\n            _logger.warning(txt)\n            asyncio.ensure_future(self._reconnect())\n        else:\n            txt = f\"Connected to {self.params.host}:{self.params.port}.\"\n            _logger.info(txt)\n            self.reset_delay()\n            return transport, protocol\n\n    def protocol_made_connection(self, protocol):\n        \"\"\"Notify successful connection.\"\"\"\n        _logger.info(\"Protocol made connection.\")\n        if not self.connected:\n            self.connected = True\n            self.protocol = protocol\n        else:\n            _logger.error(\"Factory protocol connect callback called while connected.\")\n\n    def protocol_lost_connection(self, protocol):\n        \"\"\"Notify lost connection.\"\"\"\n        _logger.info(\"Protocol lost connection.\")\n        if protocol is not self.protocol:\n            _logger.error(\n                \"Factory protocol callback called from unexpected protocol instance.\"\n            )\n\n        self.connected = False\n        self.protocol = None\n        if self.params.host:\n            asyncio.ensure_future(self._reconnect())\n\n    async def _reconnect(self):\n        \"\"\"Reconnect.\"\"\"\n        txt = f\"Waiting {self.delay_ms} ms before next connection attempt.\"\n        _logger.debug(txt)\n        await asyncio.sleep(self.delay_ms / 1000)\n        self.delay_ms = 2 * self.delay_ms\n\n        return await self._connect()\n\n\nclass ModbusTcpClient(ModbusBaseClient):\n    \"\"\"**ModbusTcpClient**.\n\n    :param host: Host IP address or host name\n    :param port: (optional) Port used for communication.\n    :param framer: (optional) Framer class.\n    :param source_address: (optional) source address of client,\n    :param kwargs: (optional) Experimental parameters\n\n    Example::\n\n        from pymodbus.client import ModbusTcpClient\n\n        async def run():\n            client = ModbusTcpClient(\"localhost\")\n\n            client.connect()\n            ...\n            client.close()\n    \"\"\"\n\n    def __init__(\n        self,\n        host: str,\n        port: int = Defaults.TcpPort,\n        framer: ModbusFramer = ModbusSocketFramer,\n        source_address: typing.Tuple[str, int] = None,\n        **kwargs: any,\n    ) -> None:\n        \"\"\"Initialize Modbus TCP Client.\"\"\"\n        super().__init__(framer=framer, **kwargs)\n        self.params.host = host\n        self.params.port = port\n        self.params.source_address = source_address\n        self.socket = None\n\n    @property\n    def connected(self):\n        \"\"\"Connect internal.\"\"\"\n        return self.connect()\n\n    def connect(self):\n        \"\"\"Connect to the modbus tcp server.\"\"\"\n        if self.socket:\n            return True\n        try:\n            self.socket = socket.create_connection(\n                (self.params.host, self.params.port),\n                timeout=self.params.timeout,\n                source_address=self.params.source_address,\n            )\n            txt = f\"Connection to Modbus server established. Socket {self.socket.getsockname()}\"\n            _logger.debug(txt)\n        except socket.error as msg:\n            txt = (\n                f\"Connection to ({self.params.host}, {self.params.port}) failed: {msg}\"\n            )\n            _logger.error(txt)\n            self.close()\n        return self.socket is not None\n\n    def close(self):\n        \"\"\"Close the underlying socket connection.\"\"\"\n        if self.socket:\n            self.socket.close()\n        self.socket = None\n\n    def _check_read_buffer(self):\n        \"\"\"Check read buffer.\"\"\"\n        time_ = time.time()\n        end = time_ + self.params.timeout\n        data = None\n        ready = select.select([self.socket], [], [], end - time_)\n        if ready[0]:\n            data = self.socket.recv(1024)\n        return data\n\n    def send(self, request):\n        \"\"\"Send data on the underlying socket.\"\"\"\n        super().send(request)\n        if not self.socket:\n            raise ConnectionException(str(self))\n        if self.state == ModbusTransactionState.RETRYING:\n            if data := self._check_read_buffer():\n                return data\n\n        if request:\n            return self.socket.send(request)\n        return 0\n\n    def recv(self, size):\n        \"\"\"Read data from the underlying descriptor.\"\"\"\n        super().recv(size)\n        if not self.socket:\n            raise ConnectionException(str(self))\n\n        # socket.recv(size) waits until it gets some data from the host but\n        # not necessarily the entire response that can be fragmented in\n        # many packets.\n        # To avoid split responses to be recognized as invalid\n        # messages and to be discarded, loops socket.recv until full data\n        # is received or timeout is expired.\n        # If timeout expires returns the read data, also if its length is\n        # less than the expected size.\n        self.socket.setblocking(0)\n\n        timeout = self.params.timeout\n\n        # If size isn\"t specified read up to 4096 bytes at a time.\n        if size is None:\n            recv_size = 4096\n        else:\n            recv_size = size\n\n        data = []\n        data_length = 0\n        time_ = time.time()\n        end = time_ + timeout\n        while recv_size > 0:\n            try:\n                ready = select.select([self.socket], [], [], end - time_)\n            except ValueError:\n                return self._handle_abrupt_socket_close(size, data, time.time() - time_)\n            if ready[0]:\n                if (recv_data := self.socket.recv(recv_size)) == b\"\":\n                    return self._handle_abrupt_socket_close(\n                        size, data, time.time() - time_\n                    )\n                data.append(recv_data)\n                data_length += len(recv_data)\n            time_ = time.time()\n\n            # If size isn\"t specified continue to read until timeout expires.\n            if size:\n                recv_size = size - data_length\n\n            # Timeout is reduced also if some data has been received in order\n            # to avoid infinite loops when there isn\"t an expected response\n            # size and the slave sends noisy data continuously.\n            if time_ > end:\n                break\n\n        return b\"\".join(data)\n\n    def _handle_abrupt_socket_close(\n        self, size, data, duration\n    ):  # pylint: disable=missing-type-doc\n        \"\"\"Handle unexpected socket close by remote end.\n\n        Intended to be invoked after determining that the remote end\n        has unexpectedly closed the connection, to clean up and handle\n        the situation appropriately.\n\n        :param size: The number of bytes that was attempted to read\n        :param data: The actual data returned\n        :param duration: Duration from the read was first attempted\n               until it was determined that the remote closed the\n               socket\n        :return: The more than zero bytes read from the remote end\n        :raises ConnectionException: If the remote end didn't send any\n                 data at all before closing the connection.\n        \"\"\"\n        self.close()\n        size_txt = size if size else \"unbounded read\"\n        readsize = f\"read of {size_txt} bytes\"\n        msg = (\n            f\"{self}: Connection unexpectedly closed \"\n            f\"{duration} seconds into {readsize}\"\n        )\n        if data:\n            result = b\"\".join(data)\n            msg += f\" after returning {len(result)} bytes\"\n            _logger.warning(msg)\n            return result\n        msg += \" without response from unit before it closed connection\"\n        raise ConnectionException(msg)\n\n    def is_socket_open(self):\n        \"\"\"Check if socket is open.\"\"\"\n        return self.socket is not None\n\n    def __str__(self):\n        \"\"\"Build a string representation of the connection.\n\n        :returns: The string representation\n        \"\"\"\n        return f\"ModbusTcpClient({self.params.host}:{self.params.port})\"\n\n    def __repr__(self):\n        \"\"\"Return string representation.\"\"\"\n        return (\n            f\"<{self.__class__.__name__} at {hex(id(self))} socket={self.socket}, \"\n            f\"ipaddr={self.params.host}, port={self.params.port}, timeout={self.params.timeout}>\"\n        )\n", "path": "pymodbus/client/tcp.py"}]}