Repository: pymodbus-dev/pymodbus

Issue:
sync modbus tcp crashed when server breaks connection (in HomeAssistant)
<!--
Please use the Pymodbus gitter channel at https://gitter.im/pymodbus_dev/Lobby or Stack Overflow(tag [pymodbus](https://stackoverflow.com/questions/tagged/pymodbus) for
support questions.

Before opening a new issue, make sure you do the following:
    * check that your issue isn't already filed: https://github.com/riptideio/pymodbus/issues
    * prepare a short, runnable example that reproduce the issue with the latest development version of Pymodbus
-->

### Versions

* Python:
verifified with 3.8, 3.9 and 3.10

* OS:
Hass

* Pymodbus:
2.53

* Modbus Hardware (if used): 
Reproduceable with software server as well.

### Pymodbus Specific
* Server: tcp/rtu/ascii - sync/async
* Client: tcp/rtu/ascii - sync/async
Client sync (modbus integration)

### Description
This is the original issue:

https://github.com/home-assistant/core/issues/68161

Hello,

I have a Victron Quattro inverter connected to Home assistant pulling data via Modbus TCP.

All works well until the inverter/charger is rebooted (say for a firmware update) or turned off/on for service work, or even a simple network issue temporarily.

Home assistant simply shows "unavailable" for all sensors until home assistant is restarted. There are no other issues and the device can stay up forever otherwise.. it is annoying as if someone is doing work or something does happen and it is not noticed the charger/inverter is not logged until home assistant is manually restarted to fix the connection issue.



### Code and Logs

+++++++++++++++++++++++++++++++++++++++++++++

My config:

Configuration.yaml

modbus: !include modbus.yaml

Modbus.yaml

 # modbus:
  - name: victron
    type: tcp
    host: 192.168.50.131  # use the IP address of your CCGX
    port: 502
    retries: 20000  ||| Makes no difference what this is set to. 
    close_comm_on_error: false |||||||||Tried this both False and the default of true
    sensors:
      - name: "Shore Power"
        scan_interval: 10
        unit_of_measurement: "W"
        slave: 100
        address: 820
 
      - name: "PV Power"
        scan_interval: 10
        unit_of_measurement: "W"
        slave: 100
        address: 850
 
      - name: "AC Loads"
        scan_interval: 10
        unit_of_measurement: "W"
        slave: 100
        address: 817

+++++++++++++++++++++++++++++++++++++++


```python
 # code and logs here.
Here is the export from the core log: 
2022-03-14 22:51:31 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/modbus/sensor.py", line 63, in async_update
    result = await self._hub.async_pymodbus_call(
  File "/usr/src/homeassistant/homeassistant/components/modbus/modbus.py", line 390, in async_pymodbus_call
    result = await self.hass.async_add_executor_job(
  File "/usr/local/lib/python3.9/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/src/homeassistant/homeassistant/components/modbus/modbus.py", line 367, in _pymodbus_call
    result = entry.func(address, value, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/client/common.py", line 114, in read_holding_registers
    return self.execute(request)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/client/sync.py", line 109, in execute
    return self.transaction.execute(request)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/transaction.py", line 174, in execute
    response, last_exception = self._transact(
  File "/usr/local/lib/python3.9/site-packages/pymodbus/transaction.py", line 294, in _transact
    result = self._recv(response_length, full)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/transaction.py", line 326, in _recv
    read_min = self.client.framer.recvPacket(min_size)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/framer/__init__.py", line 49, in recvPacket
    return self.client.recv(size)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/client/sync.py", line 89, in recv
    return self._recv(size)
  File "/usr/local/lib/python3.9/site-packages/pymodbus/client/sync.py", line 293, in _recv
    ready = select.select([self.socket], [], [], end - time_)
ValueError: filedescriptor out of range in select()
```

I personally think it is a timing problem, socket closes just before select() is called, causing select() to “see” a closed file descriptor.

Maybe it is about time to get version 3.0.0 out as release.

Let me know if I can add more information to help.



Discussion:
3.x is long pending, I am not able to find time to do that. Apologies for that. Is the remote unit explicitly closing socket before rebooting ?
Most likely not.If its inly time you miss, then I might be able to help you, let me now what is missing.

Resolving patch:
diff --git a/pymodbus/client/sync.py b/pymodbus/client/sync.py
--- a/pymodbus/client/sync.py
+++ b/pymodbus/client/sync.py
@@ -290,7 +290,11 @@ def _recv(self, size):
         time_ = time.time()
         end = time_ + timeout
         while recv_size > 0:
-            ready = select.select([self.socket], [], [], end - time_)
+            try:
+                ready = select.select([self.socket], [], [], end - time_)
+            except ValueError:
+                return self._handle_abrupt_socket_close(
+                    size, data, time.time() - time_)
             if ready[0]:
                 recv_data = self.socket.recv(recv_size)
                 if recv_data == b'':


Test patch: