We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pymodbus 3.4.0, bug in async_execute
Python: 3.8
Pymodbus: 3.4.0
used on: UDP Client (for our example connection to nowhere, or not existed address)

Bug:
https://github.com/pymodbus-dev/pymodbus/blob/dev/pymodbus/client/base.py#L193
Future is created only ONCE

code tries to use retries, first time it is ok, when more times then `wait_for` using CANCELLED future!

FIX:
move `req = self._build_response(request.transaction_id)` to wait_for

```py3
async def async_execute(self, request=None):
        """Execute requests asynchronously."""
        request.transaction_id = self.transaction.getNextTID()
        packet = self.framer.buildPacket(request)
        self.transport_send(packet)        
        if self.params.broadcast_enable and not request.slave_id:
            resp = b"Broadcast write sent - no response expected"
        else:
            count = 0
            while count < self.params.retries:
                count += 1
                try:
                    req = self._build_response(request.transaction_id)
                    resp = await asyncio.wait_for(
                        req, timeout=self.comm_params.timeout_connect
                    )
                    break
                except asyncio.exceptions.TimeoutError:
                    pass
            if count == self.params.retries:
                self.close(reconnect=True)
                raise ModbusIOException(
                    f"ERROR: No response received after {self.params.retries} retries"
                )
        return resp
```

--- 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/base.py`
Content:
```
1 """Base for all clients."""
2 from __future__ import annotations
3 
4 import asyncio
5 import socket
6 from dataclasses import dataclass
7 from typing import Any, Callable
8 
9 from pymodbus.client.mixin import ModbusClientMixin
10 from pymodbus.exceptions import ConnectionException, ModbusIOException
11 from pymodbus.factory import ClientDecoder
12 from pymodbus.framer import ModbusFramer
13 from pymodbus.logging import Log
14 from pymodbus.pdu import ModbusRequest, ModbusResponse
15 from pymodbus.transaction import DictTransactionManager
16 from pymodbus.transport import CommParams, ModbusProtocol
17 from pymodbus.utilities import ModbusTransactionState
18 
19 
20 class ModbusBaseClient(ModbusClientMixin, ModbusProtocol):
21     """**ModbusBaseClient**
22 
23     **Parameters common to all clients**:
24 
25     :param framer: (optional) Modbus Framer class.
26     :param timeout: (optional) Timeout for a request, in seconds.
27     :param retries: (optional) Max number of retries per request.
28     :param retry_on_empty: (optional) Retry on empty response.
29     :param close_comm_on_error: (optional) Close connection on error.
30     :param strict: (optional) Strict timing, 1.5 character between requests.
31     :param broadcast_enable: (optional) True to treat id 0 as broadcast address.
32     :param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.
33     :param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.
34     :param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.
35     :param kwargs: (optional) Experimental parameters.
36 
37     .. tip::
38         Common parameters and all external methods for all clients are documented here,
39         and not repeated with each client.
40 
41     .. tip::
42         **reconnect_delay** doubles automatically with each unsuccessful connect, from
43         **reconnect_delay** to **reconnect_delay_max**.
44         Set `reconnect_delay=0` to avoid automatic reconnection.
45 
46     :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.
47 
48     **Application methods, common to all clients**:
49     """
50 
51     @dataclass
52     class _params:
53         """Parameter class."""
54 
55         retries: int = None
56         retry_on_empty: bool = None
57         close_comm_on_error: bool = None
58         strict: bool = None
59         broadcast_enable: bool = None
60         reconnect_delay: int = None
61 
62         source_address: tuple[str, int] = None
63 
64         server_hostname: str = None
65 
66     def __init__(  # pylint: disable=too-many-arguments
67         self,
68         framer: type[ModbusFramer] = None,
69         timeout: float = 3,
70         retries: int = 3,
71         retry_on_empty: bool = False,
72         close_comm_on_error: bool = False,
73         strict: bool = True,
74         broadcast_enable: bool = False,
75         reconnect_delay: float = 0.1,
76         reconnect_delay_max: float = 300,
77         on_reconnect_callback: Callable[[], None] | None = None,
78         **kwargs: Any,
79     ) -> None:
80         """Initialize a client instance."""
81         ModbusClientMixin.__init__(self)
82         self.use_sync = kwargs.get("use_sync", False)
83         setup_params = CommParams(
84             comm_type=kwargs.get("CommType"),
85             comm_name="comm",
86             source_address=kwargs.get("source_address", ("0.0.0.0", 0)),
87             reconnect_delay=reconnect_delay,
88             reconnect_delay_max=reconnect_delay_max,
89             timeout_connect=timeout,
90             host=kwargs.get("host", None),
91             port=kwargs.get("port", None),
92             sslctx=kwargs.get("sslctx", None),
93             baudrate=kwargs.get("baudrate", None),
94             bytesize=kwargs.get("bytesize", None),
95             parity=kwargs.get("parity", None),
96             stopbits=kwargs.get("stopbits", None),
97             handle_local_echo=kwargs.get("handle_local_echo", False),
98         )
99         if not self.use_sync:
100             ModbusProtocol.__init__(
101                 self,
102                 setup_params,
103                 False,
104             )
105         else:
106             self.comm_params = setup_params
107         self.params = self._params()
108         self.params.retries = int(retries)
109         self.params.retry_on_empty = bool(retry_on_empty)
110         self.params.close_comm_on_error = bool(close_comm_on_error)
111         self.params.strict = bool(strict)
112         self.params.broadcast_enable = bool(broadcast_enable)
113         self.params.reconnect_delay = int(reconnect_delay)
114         self.reconnect_delay_max = int(reconnect_delay_max)
115         self.on_reconnect_callback = on_reconnect_callback
116         self.retry_on_empty: int = 0
117         # -> retry read on nothing
118 
119         self.slaves: list[int] = []
120         # -> list of acceptable slaves (0 for accept all)
121 
122         # Common variables.
123         self.framer = framer(ClientDecoder(), self)
124         self.transaction = DictTransactionManager(
125             self, retries=retries, retry_on_empty=retry_on_empty, **kwargs
126         )
127         self.reconnect_delay = self.params.reconnect_delay
128         self.reconnect_delay_current = self.params.reconnect_delay
129         self.use_udp = False
130         self.state = ModbusTransactionState.IDLE
131         self.last_frame_end: float = 0
132         self.silent_interval: float = 0
133 
134     # ----------------------------------------------------------------------- #
135     # Client external interface
136     # ----------------------------------------------------------------------- #
137     @property
138     def connected(self):
139         """Connect internal."""
140         return True
141 
142     def register(self, custom_response_class: ModbusResponse) -> None:
143         """Register a custom response class with the decoder (call **sync**).
144 
145         :param custom_response_class: (optional) Modbus response class.
146         :raises MessageRegisterException: Check exception text.
147 
148         Use register() to add non-standard responses (like e.g. a login prompt) and
149         have them interpreted automatically.
150         """
151         self.framer.decoder.register(custom_response_class)
152 
153     def close(self, reconnect=False) -> None:
154         """Close connection."""
155         if reconnect:
156             self.connection_lost(asyncio.TimeoutError("Server not responding"))
157         else:
158             self.transport_close()
159 
160     def idle_time(self) -> float:
161         """Time before initiating next transaction (call **sync**).
162 
163         Applications can call message functions without checking idle_time(),
164         this is done automatically.
165         """
166         if self.last_frame_end is None or self.silent_interval is None:
167             return 0
168         return self.last_frame_end + self.silent_interval
169 
170     def execute(self, request: ModbusRequest = None) -> ModbusResponse:
171         """Execute request and get response (call **sync/async**).
172 
173         :param request: The request to process
174         :returns: The result of the request execution
175         :raises ConnectionException: Check exception text.
176         """
177         if self.use_sync:
178             if not self.connected:
179                 raise ConnectionException(f"Failed to connect[{self!s}]")
180             return self.transaction.execute(request)
181         if not self.transport:
182             raise ConnectionException(f"Not connected[{self!s}]")
183         return self.async_execute(request)
184 
185     # ----------------------------------------------------------------------- #
186     # Merged client methods
187     # ----------------------------------------------------------------------- #
188     async def async_execute(self, request=None):
189         """Execute requests asynchronously."""
190         request.transaction_id = self.transaction.getNextTID()
191         packet = self.framer.buildPacket(request)
192         self.transport_send(packet)
193         req = self._build_response(request.transaction_id)
194         if self.params.broadcast_enable and not request.slave_id:
195             resp = b"Broadcast write sent - no response expected"
196         else:
197             count = 0
198             while count <= self.params.retries:
199                 count += 1
200                 try:
201                     req = self._build_response(request.transaction_id)
202                     resp = await asyncio.wait_for(
203                         req, timeout=self.comm_params.timeout_connect
204                     )
205                     count = 0
206                     break
207                 except asyncio.exceptions.TimeoutError:
208                     pass
209             if count > self.params.retries:
210                 self.close(reconnect=True)
211                 raise ModbusIOException(
212                     f"ERROR: No response received after {self.params.retries} retries"
213                 )
214         return resp
215 
216     def callback_data(self, data: bytes, addr: tuple = None) -> int:
217         """Handle received data
218 
219         returns number of bytes consumed
220         """
221         self.framer.processIncomingPacket(data, self._handle_response, slave=0)
222         return len(data)
223 
224     def callback_disconnected(self, _reason: Exception) -> None:
225         """Handle lost connection"""
226         for tid in list(self.transaction):
227             self.raise_future(
228                 self.transaction.getTransaction(tid),
229                 ConnectionException("Connection lost during request"),
230             )
231 
232     async def connect(self):
233         """Connect to the modbus remote host."""
234 
235     def raise_future(self, my_future, exc):
236         """Set exception of a future if not done."""
237         if not my_future.done():
238             my_future.set_exception(exc)
239 
240     def _handle_response(self, reply, **_kwargs):
241         """Handle the processed response and link to correct deferred."""
242         if reply is not None:
243             tid = reply.transaction_id
244             if handler := self.transaction.getTransaction(tid):
245                 if not handler.done():
246                     handler.set_result(reply)
247             else:
248                 Log.debug("Unrequested message: {}", reply, ":str")
249 
250     def _build_response(self, tid):
251         """Return a deferred response for the current request."""
252         my_future = asyncio.Future()
253         if not self.transport:
254             self.raise_future(my_future, ConnectionException("Client is not connected"))
255         else:
256             self.transaction.addTransaction(my_future, tid)
257         return my_future
258 
259     # ----------------------------------------------------------------------- #
260     # Internal methods
261     # ----------------------------------------------------------------------- #
262     def send(self, request):
263         """Send request.
264 
265         :meta private:
266         """
267         if self.state != ModbusTransactionState.RETRYING:
268             Log.debug('New Transaction state "SENDING"')
269             self.state = ModbusTransactionState.SENDING
270         return request
271 
272     def recv(self, size):
273         """Receive data.
274 
275         :meta private:
276         """
277         return size
278 
279     @classmethod
280     def _get_address_family(cls, address):
281         """Get the correct address family."""
282         try:
283             _ = socket.inet_pton(socket.AF_INET6, address)
284         except OSError:  # not a valid ipv6 address
285             return socket.AF_INET
286         return socket.AF_INET6
287 
288     # ----------------------------------------------------------------------- #
289     # The magic methods
290     # ----------------------------------------------------------------------- #
291     def __enter__(self):
292         """Implement the client with enter block.
293 
294         :returns: The current instance of the client
295         :raises ConnectionException:
296         """
297 
298         if not self.connect():
299             raise ConnectionException(f"Failed to connect[{self.__str__()}]")
300         return self
301 
302     async def __aenter__(self):
303         """Implement the client with enter block.
304 
305         :returns: The current instance of the client
306         :raises ConnectionException:
307         """
308         if not await self.connect():
309             raise ConnectionException(f"Failed to connect[{self.__str__()}]")
310         return self
311 
312     def __exit__(self, klass, value, traceback):
313         """Implement the client with exit block."""
314         self.close()
315 
316     async def __aexit__(self, klass, value, traceback):
317         """Implement the client with exit block."""
318         self.close()
319 
320     def __str__(self):
321         """Build a string representation of the connection.
322 
323         :returns: The string representation
324         """
325         return (
326             f"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}"
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/base.py b/pymodbus/client/base.py
--- a/pymodbus/client/base.py
+++ b/pymodbus/client/base.py
@@ -190,22 +190,19 @@
         request.transaction_id = self.transaction.getNextTID()
         packet = self.framer.buildPacket(request)
         self.transport_send(packet)
-        req = self._build_response(request.transaction_id)
         if self.params.broadcast_enable and not request.slave_id:
             resp = b"Broadcast write sent - no response expected"
         else:
             count = 0
             while count <= self.params.retries:
-                count += 1
                 try:
                     req = self._build_response(request.transaction_id)
                     resp = await asyncio.wait_for(
                         req, timeout=self.comm_params.timeout_connect
                     )
-                    count = 0
                     break
                 except asyncio.exceptions.TimeoutError:
-                    pass
+                    count += 1
             if count > self.params.retries:
                 self.close(reconnect=True)
                 raise ModbusIOException(

{"golden_diff": "diff --git a/pymodbus/client/base.py b/pymodbus/client/base.py\n--- a/pymodbus/client/base.py\n+++ b/pymodbus/client/base.py\n@@ -190,22 +190,19 @@\n         request.transaction_id = self.transaction.getNextTID()\n         packet = self.framer.buildPacket(request)\n         self.transport_send(packet)\n-        req = self._build_response(request.transaction_id)\n         if self.params.broadcast_enable and not request.slave_id:\n             resp = b\"Broadcast write sent - no response expected\"\n         else:\n             count = 0\n             while count <= self.params.retries:\n-                count += 1\n                 try:\n                     req = self._build_response(request.transaction_id)\n                     resp = await asyncio.wait_for(\n                         req, timeout=self.comm_params.timeout_connect\n                     )\n-                    count = 0\n                     break\n                 except asyncio.exceptions.TimeoutError:\n-                    pass\n+                    count += 1\n             if count > self.params.retries:\n                 self.close(reconnect=True)\n                 raise ModbusIOException(\n", "issue": "Pymodbus 3.4.0, bug in async_execute\nPython: 3.8\r\nPymodbus: 3.4.0\r\nused on: UDP Client (for our example connection to nowhere, or not existed address)\r\n\r\nBug:\r\nhttps://github.com/pymodbus-dev/pymodbus/blob/dev/pymodbus/client/base.py#L193\r\nFuture is created only ONCE\r\n\r\ncode tries to use retries, first time it is ok, when more times then `wait_for` using CANCELLED future!\r\n\r\nFIX:\r\nmove `req = self._build_response(request.transaction_id)` to wait_for\r\n\r\n```py3\r\nasync def async_execute(self, request=None):\r\n        \"\"\"Execute requests asynchronously.\"\"\"\r\n        request.transaction_id = self.transaction.getNextTID()\r\n        packet = self.framer.buildPacket(request)\r\n        self.transport_send(packet)        \r\n        if self.params.broadcast_enable and not request.slave_id:\r\n            resp = b\"Broadcast write sent - no response expected\"\r\n        else:\r\n            count = 0\r\n            while count < self.params.retries:\r\n                count += 1\r\n                try:\r\n                    req = self._build_response(request.transaction_id)\r\n                    resp = await asyncio.wait_for(\r\n                        req, timeout=self.comm_params.timeout_connect\r\n                    )\r\n                    break\r\n                except asyncio.exceptions.TimeoutError:\r\n                    pass\r\n            if count == self.params.retries:\r\n                self.close(reconnect=True)\r\n                raise ModbusIOException(\r\n                    f\"ERROR: No response received after {self.params.retries} retries\"\r\n                )\r\n        return resp\r\n```\n", "before_files": [{"content": "\"\"\"Base for all clients.\"\"\"\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nfrom dataclasses import dataclass\nfrom typing import Any, Callable\n\nfrom pymodbus.client.mixin import ModbusClientMixin\nfrom pymodbus.exceptions import ConnectionException, ModbusIOException\nfrom pymodbus.factory import ClientDecoder\nfrom pymodbus.framer import ModbusFramer\nfrom pymodbus.logging import Log\nfrom pymodbus.pdu import ModbusRequest, ModbusResponse\nfrom pymodbus.transaction import DictTransactionManager\nfrom pymodbus.transport import CommParams, ModbusProtocol\nfrom pymodbus.utilities import ModbusTransactionState\n\n\nclass ModbusBaseClient(ModbusClientMixin, ModbusProtocol):\n    \"\"\"**ModbusBaseClient**\n\n    **Parameters common to all clients**:\n\n    :param framer: (optional) Modbus Framer class.\n    :param timeout: (optional) Timeout for a request, in seconds.\n    :param retries: (optional) Max number of retries per request.\n    :param retry_on_empty: (optional) Retry on empty response.\n    :param close_comm_on_error: (optional) Close connection on error.\n    :param strict: (optional) Strict timing, 1.5 character between requests.\n    :param broadcast_enable: (optional) True to treat id 0 as broadcast address.\n    :param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.\n    :param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.\n    :param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.\n    :param kwargs: (optional) Experimental parameters.\n\n    .. tip::\n        Common parameters and all external methods for all clients are documented here,\n        and not repeated with each client.\n\n    .. tip::\n        **reconnect_delay** doubles automatically with each unsuccessful connect, from\n        **reconnect_delay** to **reconnect_delay_max**.\n        Set `reconnect_delay=0` to avoid automatic reconnection.\n\n    :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.\n\n    **Application methods, common to all clients**:\n    \"\"\"\n\n    @dataclass\n    class _params:\n        \"\"\"Parameter class.\"\"\"\n\n        retries: int = None\n        retry_on_empty: bool = None\n        close_comm_on_error: bool = None\n        strict: bool = None\n        broadcast_enable: bool = None\n        reconnect_delay: int = None\n\n        source_address: tuple[str, int] = None\n\n        server_hostname: str = None\n\n    def __init__(  # pylint: disable=too-many-arguments\n        self,\n        framer: type[ModbusFramer] = None,\n        timeout: float = 3,\n        retries: int = 3,\n        retry_on_empty: bool = False,\n        close_comm_on_error: bool = False,\n        strict: bool = True,\n        broadcast_enable: bool = False,\n        reconnect_delay: float = 0.1,\n        reconnect_delay_max: float = 300,\n        on_reconnect_callback: Callable[[], None] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize a client instance.\"\"\"\n        ModbusClientMixin.__init__(self)\n        self.use_sync = kwargs.get(\"use_sync\", False)\n        setup_params = CommParams(\n            comm_type=kwargs.get(\"CommType\"),\n            comm_name=\"comm\",\n            source_address=kwargs.get(\"source_address\", (\"0.0.0.0\", 0)),\n            reconnect_delay=reconnect_delay,\n            reconnect_delay_max=reconnect_delay_max,\n            timeout_connect=timeout,\n            host=kwargs.get(\"host\", None),\n            port=kwargs.get(\"port\", None),\n            sslctx=kwargs.get(\"sslctx\", None),\n            baudrate=kwargs.get(\"baudrate\", None),\n            bytesize=kwargs.get(\"bytesize\", None),\n            parity=kwargs.get(\"parity\", None),\n            stopbits=kwargs.get(\"stopbits\", None),\n            handle_local_echo=kwargs.get(\"handle_local_echo\", False),\n        )\n        if not self.use_sync:\n            ModbusProtocol.__init__(\n                self,\n                setup_params,\n                False,\n            )\n        else:\n            self.comm_params = setup_params\n        self.params = self._params()\n        self.params.retries = int(retries)\n        self.params.retry_on_empty = bool(retry_on_empty)\n        self.params.close_comm_on_error = bool(close_comm_on_error)\n        self.params.strict = bool(strict)\n        self.params.broadcast_enable = bool(broadcast_enable)\n        self.params.reconnect_delay = int(reconnect_delay)\n        self.reconnect_delay_max = int(reconnect_delay_max)\n        self.on_reconnect_callback = on_reconnect_callback\n        self.retry_on_empty: int = 0\n        # -> retry read on nothing\n\n        self.slaves: list[int] = []\n        # -> list of acceptable slaves (0 for accept all)\n\n        # Common variables.\n        self.framer = framer(ClientDecoder(), self)\n        self.transaction = DictTransactionManager(\n            self, retries=retries, retry_on_empty=retry_on_empty, **kwargs\n        )\n        self.reconnect_delay = self.params.reconnect_delay\n        self.reconnect_delay_current = self.params.reconnect_delay\n        self.use_udp = False\n        self.state = ModbusTransactionState.IDLE\n        self.last_frame_end: float = 0\n        self.silent_interval: float = 0\n\n    # ----------------------------------------------------------------------- #\n    # Client external interface\n    # ----------------------------------------------------------------------- #\n    @property\n    def connected(self):\n        \"\"\"Connect internal.\"\"\"\n        return True\n\n    def register(self, custom_response_class: ModbusResponse) -> None:\n        \"\"\"Register a custom response class with the decoder (call **sync**).\n\n        :param custom_response_class: (optional) Modbus response class.\n        :raises MessageRegisterException: Check exception text.\n\n        Use register() to add non-standard responses (like e.g. a login prompt) and\n        have them interpreted automatically.\n        \"\"\"\n        self.framer.decoder.register(custom_response_class)\n\n    def close(self, reconnect=False) -> None:\n        \"\"\"Close connection.\"\"\"\n        if reconnect:\n            self.connection_lost(asyncio.TimeoutError(\"Server not responding\"))\n        else:\n            self.transport_close()\n\n    def idle_time(self) -> float:\n        \"\"\"Time before initiating next transaction (call **sync**).\n\n        Applications can call message functions without checking idle_time(),\n        this is done automatically.\n        \"\"\"\n        if self.last_frame_end is None or self.silent_interval is None:\n            return 0\n        return self.last_frame_end + self.silent_interval\n\n    def execute(self, request: ModbusRequest = None) -> ModbusResponse:\n        \"\"\"Execute request and get response (call **sync/async**).\n\n        :param request: The request to process\n        :returns: The result of the request execution\n        :raises ConnectionException: Check exception text.\n        \"\"\"\n        if self.use_sync:\n            if not self.connected:\n                raise ConnectionException(f\"Failed to connect[{self!s}]\")\n            return self.transaction.execute(request)\n        if not self.transport:\n            raise ConnectionException(f\"Not connected[{self!s}]\")\n        return self.async_execute(request)\n\n    # ----------------------------------------------------------------------- #\n    # Merged client methods\n    # ----------------------------------------------------------------------- #\n    async def async_execute(self, request=None):\n        \"\"\"Execute requests asynchronously.\"\"\"\n        request.transaction_id = self.transaction.getNextTID()\n        packet = self.framer.buildPacket(request)\n        self.transport_send(packet)\n        req = self._build_response(request.transaction_id)\n        if self.params.broadcast_enable and not request.slave_id:\n            resp = b\"Broadcast write sent - no response expected\"\n        else:\n            count = 0\n            while count <= self.params.retries:\n                count += 1\n                try:\n                    req = self._build_response(request.transaction_id)\n                    resp = await asyncio.wait_for(\n                        req, timeout=self.comm_params.timeout_connect\n                    )\n                    count = 0\n                    break\n                except asyncio.exceptions.TimeoutError:\n                    pass\n            if count > self.params.retries:\n                self.close(reconnect=True)\n                raise ModbusIOException(\n                    f\"ERROR: No response received after {self.params.retries} retries\"\n                )\n        return resp\n\n    def callback_data(self, data: bytes, addr: tuple = None) -> int:\n        \"\"\"Handle received data\n\n        returns number of bytes consumed\n        \"\"\"\n        self.framer.processIncomingPacket(data, self._handle_response, slave=0)\n        return len(data)\n\n    def callback_disconnected(self, _reason: Exception) -> None:\n        \"\"\"Handle lost connection\"\"\"\n        for tid in list(self.transaction):\n            self.raise_future(\n                self.transaction.getTransaction(tid),\n                ConnectionException(\"Connection lost during request\"),\n            )\n\n    async def connect(self):\n        \"\"\"Connect to the modbus remote host.\"\"\"\n\n    def raise_future(self, my_future, exc):\n        \"\"\"Set exception of a future if not done.\"\"\"\n        if not my_future.done():\n            my_future.set_exception(exc)\n\n    def _handle_response(self, reply, **_kwargs):\n        \"\"\"Handle the processed response and link to correct deferred.\"\"\"\n        if reply is not None:\n            tid = reply.transaction_id\n            if handler := self.transaction.getTransaction(tid):\n                if not handler.done():\n                    handler.set_result(reply)\n            else:\n                Log.debug(\"Unrequested message: {}\", reply, \":str\")\n\n    def _build_response(self, tid):\n        \"\"\"Return a deferred response for the current request.\"\"\"\n        my_future = asyncio.Future()\n        if not self.transport:\n            self.raise_future(my_future, ConnectionException(\"Client is not connected\"))\n        else:\n            self.transaction.addTransaction(my_future, tid)\n        return my_future\n\n    # ----------------------------------------------------------------------- #\n    # Internal methods\n    # ----------------------------------------------------------------------- #\n    def send(self, request):\n        \"\"\"Send request.\n\n        :meta private:\n        \"\"\"\n        if self.state != ModbusTransactionState.RETRYING:\n            Log.debug('New Transaction state \"SENDING\"')\n            self.state = ModbusTransactionState.SENDING\n        return request\n\n    def recv(self, size):\n        \"\"\"Receive data.\n\n        :meta private:\n        \"\"\"\n        return size\n\n    @classmethod\n    def _get_address_family(cls, address):\n        \"\"\"Get the correct address family.\"\"\"\n        try:\n            _ = socket.inet_pton(socket.AF_INET6, address)\n        except OSError:  # not a valid ipv6 address\n            return socket.AF_INET\n        return socket.AF_INET6\n\n    # ----------------------------------------------------------------------- #\n    # The magic methods\n    # ----------------------------------------------------------------------- #\n    def __enter__(self):\n        \"\"\"Implement the client with enter block.\n\n        :returns: The current instance of the client\n        :raises ConnectionException:\n        \"\"\"\n\n        if not self.connect():\n            raise ConnectionException(f\"Failed to connect[{self.__str__()}]\")\n        return self\n\n    async def __aenter__(self):\n        \"\"\"Implement the client with enter block.\n\n        :returns: The current instance of the client\n        :raises ConnectionException:\n        \"\"\"\n        if not await self.connect():\n            raise ConnectionException(f\"Failed to connect[{self.__str__()}]\")\n        return self\n\n    def __exit__(self, klass, value, traceback):\n        \"\"\"Implement the client with exit block.\"\"\"\n        self.close()\n\n    async def __aexit__(self, klass, value, traceback):\n        \"\"\"Implement the client with exit block.\"\"\"\n        self.close()\n\n    def __str__(self):\n        \"\"\"Build a string representation of the connection.\n\n        :returns: The string representation\n        \"\"\"\n        return (\n            f\"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}\"\n        )\n", "path": "pymodbus/client/base.py"}], "after_files": [{"content": "\"\"\"Base for all clients.\"\"\"\nfrom __future__ import annotations\n\nimport asyncio\nimport socket\nfrom dataclasses import dataclass\nfrom typing import Any, Callable\n\nfrom pymodbus.client.mixin import ModbusClientMixin\nfrom pymodbus.exceptions import ConnectionException, ModbusIOException\nfrom pymodbus.factory import ClientDecoder\nfrom pymodbus.framer import ModbusFramer\nfrom pymodbus.logging import Log\nfrom pymodbus.pdu import ModbusRequest, ModbusResponse\nfrom pymodbus.transaction import DictTransactionManager\nfrom pymodbus.transport import CommParams, ModbusProtocol\nfrom pymodbus.utilities import ModbusTransactionState\n\n\nclass ModbusBaseClient(ModbusClientMixin, ModbusProtocol):\n    \"\"\"**ModbusBaseClient**\n\n    **Parameters common to all clients**:\n\n    :param framer: (optional) Modbus Framer class.\n    :param timeout: (optional) Timeout for a request, in seconds.\n    :param retries: (optional) Max number of retries per request.\n    :param retry_on_empty: (optional) Retry on empty response.\n    :param close_comm_on_error: (optional) Close connection on error.\n    :param strict: (optional) Strict timing, 1.5 character between requests.\n    :param broadcast_enable: (optional) True to treat id 0 as broadcast address.\n    :param reconnect_delay: (optional) Minimum delay in milliseconds before reconnecting.\n    :param reconnect_delay_max: (optional) Maximum delay in milliseconds before reconnecting.\n    :param on_reconnect_callback: (optional) Function that will be called just before a reconnection attempt.\n    :param kwargs: (optional) Experimental parameters.\n\n    .. tip::\n        Common parameters and all external methods for all clients are documented here,\n        and not repeated with each client.\n\n    .. tip::\n        **reconnect_delay** doubles automatically with each unsuccessful connect, from\n        **reconnect_delay** to **reconnect_delay_max**.\n        Set `reconnect_delay=0` to avoid automatic reconnection.\n\n    :mod:`ModbusBaseClient` is normally not referenced outside :mod:`pymodbus`.\n\n    **Application methods, common to all clients**:\n    \"\"\"\n\n    @dataclass\n    class _params:\n        \"\"\"Parameter class.\"\"\"\n\n        retries: int = None\n        retry_on_empty: bool = None\n        close_comm_on_error: bool = None\n        strict: bool = None\n        broadcast_enable: bool = None\n        reconnect_delay: int = None\n\n        source_address: tuple[str, int] = None\n\n        server_hostname: str = None\n\n    def __init__(  # pylint: disable=too-many-arguments\n        self,\n        framer: type[ModbusFramer] = None,\n        timeout: float = 3,\n        retries: int = 3,\n        retry_on_empty: bool = False,\n        close_comm_on_error: bool = False,\n        strict: bool = True,\n        broadcast_enable: bool = False,\n        reconnect_delay: float = 0.1,\n        reconnect_delay_max: float = 300,\n        on_reconnect_callback: Callable[[], None] | None = None,\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Initialize a client instance.\"\"\"\n        ModbusClientMixin.__init__(self)\n        self.use_sync = kwargs.get(\"use_sync\", False)\n        setup_params = CommParams(\n            comm_type=kwargs.get(\"CommType\"),\n            comm_name=\"comm\",\n            source_address=kwargs.get(\"source_address\", (\"0.0.0.0\", 0)),\n            reconnect_delay=reconnect_delay,\n            reconnect_delay_max=reconnect_delay_max,\n            timeout_connect=timeout,\n            host=kwargs.get(\"host\", None),\n            port=kwargs.get(\"port\", None),\n            sslctx=kwargs.get(\"sslctx\", None),\n            baudrate=kwargs.get(\"baudrate\", None),\n            bytesize=kwargs.get(\"bytesize\", None),\n            parity=kwargs.get(\"parity\", None),\n            stopbits=kwargs.get(\"stopbits\", None),\n            handle_local_echo=kwargs.get(\"handle_local_echo\", False),\n        )\n        if not self.use_sync:\n            ModbusProtocol.__init__(\n                self,\n                setup_params,\n                False,\n            )\n        else:\n            self.comm_params = setup_params\n        self.params = self._params()\n        self.params.retries = int(retries)\n        self.params.retry_on_empty = bool(retry_on_empty)\n        self.params.close_comm_on_error = bool(close_comm_on_error)\n        self.params.strict = bool(strict)\n        self.params.broadcast_enable = bool(broadcast_enable)\n        self.params.reconnect_delay = int(reconnect_delay)\n        self.reconnect_delay_max = int(reconnect_delay_max)\n        self.on_reconnect_callback = on_reconnect_callback\n        self.retry_on_empty: int = 0\n        # -> retry read on nothing\n\n        self.slaves: list[int] = []\n        # -> list of acceptable slaves (0 for accept all)\n\n        # Common variables.\n        self.framer = framer(ClientDecoder(), self)\n        self.transaction = DictTransactionManager(\n            self, retries=retries, retry_on_empty=retry_on_empty, **kwargs\n        )\n        self.reconnect_delay = self.params.reconnect_delay\n        self.reconnect_delay_current = self.params.reconnect_delay\n        self.use_udp = False\n        self.state = ModbusTransactionState.IDLE\n        self.last_frame_end: float = 0\n        self.silent_interval: float = 0\n\n    # ----------------------------------------------------------------------- #\n    # Client external interface\n    # ----------------------------------------------------------------------- #\n    @property\n    def connected(self):\n        \"\"\"Connect internal.\"\"\"\n        return True\n\n    def register(self, custom_response_class: ModbusResponse) -> None:\n        \"\"\"Register a custom response class with the decoder (call **sync**).\n\n        :param custom_response_class: (optional) Modbus response class.\n        :raises MessageRegisterException: Check exception text.\n\n        Use register() to add non-standard responses (like e.g. a login prompt) and\n        have them interpreted automatically.\n        \"\"\"\n        self.framer.decoder.register(custom_response_class)\n\n    def close(self, reconnect=False) -> None:\n        \"\"\"Close connection.\"\"\"\n        if reconnect:\n            self.connection_lost(asyncio.TimeoutError(\"Server not responding\"))\n        else:\n            self.transport_close()\n\n    def idle_time(self) -> float:\n        \"\"\"Time before initiating next transaction (call **sync**).\n\n        Applications can call message functions without checking idle_time(),\n        this is done automatically.\n        \"\"\"\n        if self.last_frame_end is None or self.silent_interval is None:\n            return 0\n        return self.last_frame_end + self.silent_interval\n\n    def execute(self, request: ModbusRequest = None) -> ModbusResponse:\n        \"\"\"Execute request and get response (call **sync/async**).\n\n        :param request: The request to process\n        :returns: The result of the request execution\n        :raises ConnectionException: Check exception text.\n        \"\"\"\n        if self.use_sync:\n            if not self.connected:\n                raise ConnectionException(f\"Failed to connect[{self!s}]\")\n            return self.transaction.execute(request)\n        if not self.transport:\n            raise ConnectionException(f\"Not connected[{self!s}]\")\n        return self.async_execute(request)\n\n    # ----------------------------------------------------------------------- #\n    # Merged client methods\n    # ----------------------------------------------------------------------- #\n    async def async_execute(self, request=None):\n        \"\"\"Execute requests asynchronously.\"\"\"\n        request.transaction_id = self.transaction.getNextTID()\n        packet = self.framer.buildPacket(request)\n        self.transport_send(packet)\n        if self.params.broadcast_enable and not request.slave_id:\n            resp = b\"Broadcast write sent - no response expected\"\n        else:\n            count = 0\n            while count <= self.params.retries:\n                try:\n                    req = self._build_response(request.transaction_id)\n                    resp = await asyncio.wait_for(\n                        req, timeout=self.comm_params.timeout_connect\n                    )\n                    break\n                except asyncio.exceptions.TimeoutError:\n                    count += 1\n            if count > self.params.retries:\n                self.close(reconnect=True)\n                raise ModbusIOException(\n                    f\"ERROR: No response received after {self.params.retries} retries\"\n                )\n        return resp\n\n    def callback_data(self, data: bytes, addr: tuple = None) -> int:\n        \"\"\"Handle received data\n\n        returns number of bytes consumed\n        \"\"\"\n        self.framer.processIncomingPacket(data, self._handle_response, slave=0)\n        return len(data)\n\n    def callback_disconnected(self, _reason: Exception) -> None:\n        \"\"\"Handle lost connection\"\"\"\n        for tid in list(self.transaction):\n            self.raise_future(\n                self.transaction.getTransaction(tid),\n                ConnectionException(\"Connection lost during request\"),\n            )\n\n    async def connect(self):\n        \"\"\"Connect to the modbus remote host.\"\"\"\n\n    def raise_future(self, my_future, exc):\n        \"\"\"Set exception of a future if not done.\"\"\"\n        if not my_future.done():\n            my_future.set_exception(exc)\n\n    def _handle_response(self, reply, **_kwargs):\n        \"\"\"Handle the processed response and link to correct deferred.\"\"\"\n        if reply is not None:\n            tid = reply.transaction_id\n            if handler := self.transaction.getTransaction(tid):\n                if not handler.done():\n                    handler.set_result(reply)\n            else:\n                Log.debug(\"Unrequested message: {}\", reply, \":str\")\n\n    def _build_response(self, tid):\n        \"\"\"Return a deferred response for the current request.\"\"\"\n        my_future = asyncio.Future()\n        if not self.transport:\n            self.raise_future(my_future, ConnectionException(\"Client is not connected\"))\n        else:\n            self.transaction.addTransaction(my_future, tid)\n        return my_future\n\n    # ----------------------------------------------------------------------- #\n    # Internal methods\n    # ----------------------------------------------------------------------- #\n    def send(self, request):\n        \"\"\"Send request.\n\n        :meta private:\n        \"\"\"\n        if self.state != ModbusTransactionState.RETRYING:\n            Log.debug('New Transaction state \"SENDING\"')\n            self.state = ModbusTransactionState.SENDING\n        return request\n\n    def recv(self, size):\n        \"\"\"Receive data.\n\n        :meta private:\n        \"\"\"\n        return size\n\n    @classmethod\n    def _get_address_family(cls, address):\n        \"\"\"Get the correct address family.\"\"\"\n        try:\n            _ = socket.inet_pton(socket.AF_INET6, address)\n        except OSError:  # not a valid ipv6 address\n            return socket.AF_INET\n        return socket.AF_INET6\n\n    # ----------------------------------------------------------------------- #\n    # The magic methods\n    # ----------------------------------------------------------------------- #\n    def __enter__(self):\n        \"\"\"Implement the client with enter block.\n\n        :returns: The current instance of the client\n        :raises ConnectionException:\n        \"\"\"\n\n        if not self.connect():\n            raise ConnectionException(f\"Failed to connect[{self.__str__()}]\")\n        return self\n\n    async def __aenter__(self):\n        \"\"\"Implement the client with enter block.\n\n        :returns: The current instance of the client\n        :raises ConnectionException:\n        \"\"\"\n        if not await self.connect():\n            raise ConnectionException(f\"Failed to connect[{self.__str__()}]\")\n        return self\n\n    def __exit__(self, klass, value, traceback):\n        \"\"\"Implement the client with exit block.\"\"\"\n        self.close()\n\n    async def __aexit__(self, klass, value, traceback):\n        \"\"\"Implement the client with exit block.\"\"\"\n        self.close()\n\n    def __str__(self):\n        \"\"\"Build a string representation of the connection.\n\n        :returns: The string representation\n        \"\"\"\n        return (\n            f\"{self.__class__.__name__} {self.comm_params.host}:{self.comm_params.port}\"\n        )\n", "path": "pymodbus/client/base.py"}]}