Repository: pymodbus-dev/pymodbus

Issue:
AttributeError: 'AsyncModbusTcpClient' object has no attribute 'loop'
### Versions

- Python: 3.9.16 (under pyenv)
- OS: raspbian 10
- Pymodbus: 3.3.0

### Pymodbus Specific

- Client: tcp - async

### Description

When calling client.open(), an exception is raised.

### Code and Logs

```python
  async def connect(self):
    logging.debug('PyModbusClient: *' + self.eqConfig['name'] + '* connect called')
    try:
      if not self.connected or not self.client.connected:
        logging.debug('PyModbusClient: *' + self.eqConfig['name'] + '* connecting...')
        await self.client.connect()
      ret = True
    except Exception as e:
      logging.error('PyModbusClient: *' + self.eqConfig['name'] + '* Something went wrong while connecting to equipment id ' + self.eqConfig['id'] + ': ' + repr(e)) # + ' - ' + e.string)
      ret = False
    delay = float(self.eqConfig['eqFirstDelay'])
    await asyncio.sleep(delay)
    return ret
```

Traceback, when I don't catch the exception:

```
Traceback (most recent call last):
File "/var/www/html/plugins/mymodbus/ressources/mymodbusd/mymodbus.py", line 441, in connect
await self.client.connect()
File "/var/www/html/plugins/mymodbus/ressources/_pyenv/versions/3.9.16/lib/python3.9/site-packages/pymodbus/client/tcp.py", line 70, in connect
return await self.transport_connect()
File "/var/www/html/plugins/mymodbus/ressources/_pyenv/versions/3.9.16/lib/python3.9/site-packages/pymodbus/transport/transport.py", line 268, in transport_connect
self.call_connect_listen(),
File "/var/www/html/plugins/mymodbus/ressources/_pyenv/versions/3.9.16/lib/python3.9/site-packages/pymodbus/transport/transport.py", line 140, in 
self.call_connect_listen = lambda: self.loop.create_connection(
AttributeError: 'AsyncModbusTcpClient' object has no attribute 'loop'
```


Discussion:
This happens if there are no active asyncio loop, when you make an instance of AsyncModbus*.

In short the function / method where you create an object of AsyncModbus* must be async.

It is not detected as and error until you call connect (the first async call).
I am thinking of a fix for v3.3.1, but I am not sure it is 100%.

Resolving patch:
diff --git a/pymodbus/transport/transport.py b/pymodbus/transport/transport.py
--- a/pymodbus/transport/transport.py
+++ b/pymodbus/transport/transport.py
@@ -93,8 +93,9 @@ def __init__(
         self.reconnect_delay_current: float = 0
         self.transport: asyncio.BaseTransport | asyncio.Server = None
         self.protocol: asyncio.BaseProtocol = None
+        self.loop: asyncio.AbstractEventLoop = None
         with suppress(RuntimeError):
-            self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
+            self.loop = asyncio.get_running_loop()
         self.reconnect_timer: asyncio.Task = None
         self.recv_buffer: bytes = b""
         self.call_connect_listen: Callable[[], Coroutine[Any, Any, Any]] = lambda: None
@@ -263,6 +264,8 @@ def setup_serial(
     async def transport_connect(self):
         """Handle generic connect and call on to specific transport connect."""
         Log.debug("Connecting {}", self.comm_params.comm_name)
+        if not self.loop:
+            self.loop = asyncio.get_running_loop()
         self.transport, self.protocol = None, None
         try:
             self.transport, self.protocol = await asyncio.wait_for(
@@ -296,6 +299,8 @@ def connection_made(self, transport: asyncio.BaseTransport):
         :param transport: socket etc. representing the connection.
         """
         Log.debug("Connected to {}", self.comm_params.comm_name)
+        if not self.loop:
+            self.loop = asyncio.get_running_loop()
         self.transport = transport
         self.reset_delay()
         self.cb_connection_made()


Test patch: