Repository: home-assistant-ecosystem/python-luftdaten (language: python)

Issue:
Many timeout exceptions
I'm using this library via the Home Assistant (2202.8.6) integration, to gather data from multiple sensors.

The Luftdaten API tends to be slow. My logs are spammed with multiple timeout exceptions that are not handled properly, and it also causes the Home Assistant sensors to go offline.

* Would it be an idea to increase the timeout from 5 seconds (default of httpx) to something higher? Maybe 10 or 15 seconds?
* Can timeout exceptions be ignored, such that they do not make the sensor go offline?




Discussion / hints:
Here is an example exception, from the Home Assistant logs:

```
2022-08-23 22:25:48.074 ERROR (MainThread) [homeassistant.components.luftdaten] Unexpected error fetching luftdaten_70908 data:
Traceback (most recent call last):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/anyio/streams/tls.py", line 130, in _call_sslobject_method
    result = func(*args)
  File "/usr/lib/python3.10/ssl.py", line 916, in read
    v = self._sslobj.read(len)
ssl.SSLWantReadError: The operation did not complete (read) (_ssl.c:2548)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/backends/asyncio.py", line 33, in read
    return await self._stream.receive(max_bytes=max_bytes)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/anyio/streams/tls.py", line 195, in receive
    data = await self._call_sslobject_method(self._ssl_object.read, max_bytes)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/anyio/streams/tls.py", line 137, in _call_sslobject_method
    data = await self.transport_stream.receive()
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 1265, in receive
    await self._protocol.read_event.wait()
  File "/usr/lib/python3.10/asyncio/locks.py", line 214, in wait
    await fut
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_exceptions.py", line 8, in map_exceptions
    yield
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/backends/asyncio.py", line 31, in read
    with anyio.fail_after(timeout):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/anyio/_core/_tasks.py", line 118, in __exit__
    raise TimeoutError
TimeoutError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_transports/default.py", line 60, in map_httpcore_exceptions
    yield
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_transports/default.py", line 353, in handle_async_request
    resp = await self._pool.handle_async_request(req)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 253, in handle_async_request
    raise exc
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/connection_pool.py", line 237, in handle_async_request
    response = await connection.handle_async_request(request)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/connection.py", line 90, in handle_async_request
    return await self._connection.handle_async_request(request)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/http11.py", line 105, in handle_async_request
    raise exc
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/http11.py", line 84, in handle_async_request
    ) = await self._receive_response_headers(**kwargs)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/http11.py", line 148, in _receive_response_headers
    event = await self._receive_event(timeout=timeout)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_async/http11.py", line 177, in _receive_event
    data = await self._network_stream.read(
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/backends/asyncio.py", line 30, in read
    with map_exceptions(exc_map):
  File "/usr/lib/python3.10/contextlib.py", line 153, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpcore/_exceptions.py", line 12, in map_exceptions
    raise to_exc(exc)
httpcore.ReadTimeout

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/homeassistant/helpers/update_coordinator.py", line 205, in _async_refresh
    self.data = await self._async_update_data()
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/homeassistant/helpers/update_coordinator.py", line 164, in _async_update_data
    return await self.update_method()
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/homeassistant/components/luftdaten/__init__.py", line 43, in async_update
    await sensor_community.get_data()
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/luftdaten/__init__.py", line 29, in get_data
    response = await client.get(str(url))
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1751, in get
    return await self.request(
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1527, in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1614, in send
    response = await self._send_handling_auth(
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1642, in _send_handling_auth
    response = await self._send_handling_redirects(
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1679, in _send_handling_redirects
    response = await self._send_single_request(request)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_client.py", line 1716, in _send_single_request
    response = await transport.handle_async_request(request)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_transports/default.py", line 352, in handle_async_request
    with map_httpcore_exceptions():
  File "/usr/lib/python3.10/contextlib.py", line 153, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/opt/apps/homeassistant/env/lib/python3.10/site-packages/httpx/_transports/default.py", line 77, in map_httpcore_exceptions
    raise mapped_exc(message) from exc
httpx.ReadTimeout
```

Fix patch:
diff --git a/luftdaten/__init__.py b/luftdaten/__init__.py
index 0d528c2..1c4a15f 100644
--- a/luftdaten/__init__.py
+++ b/luftdaten/__init__.py
@@ -29,6 +29,10 @@ async def get_data(self):
                 response = await client.get(str(url))
         except httpx.ConnectError:
             raise exceptions.LuftdatenConnectionError(f"Connection to {url} failed")
+        except httpx.ConnectTimeout:
+            raise exceptions.LuftdatenConnectionError(f"Connection to {url} timed out")
+        except httpx.ReadTimeout:
+            raise exceptions.LuftdatenConnectionError(f"Read timeout from {url}")
 
         if response.status_code == httpx.codes.OK:
             try:


Test patch:
diff --git a/tests/test_timeout.py b/tests/test_timeout.py
index 2273233..69559ae 100644
--- a/tests/test_timeout.py
+++ b/tests/test_timeout.py
@@ -4,12 +4,29 @@
 from pytest_httpx import HTTPXMock
 
 from luftdaten import Luftdaten
+from luftdaten.exceptions import LuftdatenConnectionError
 
 SENSOR_ID = 1
 
 @pytest.mark.asyncio
-async def test_timeout(httpx_mock: HTTPXMock):
-    """Test if the connection is hitting the timeout."""
+async def test_connect_timeout(httpx_mock: HTTPXMock):
+    """Test if the connection is hitting the timeout during connect."""
+
+    def raise_timeout(request):
+        """Set the timeout for the requests."""
+        raise httpx.ConnectTimeout(
+            f"Unable to connect within {request.extensions['timeout']}", request=request
+        )
+
+    httpx_mock.add_callback(raise_timeout)
+
+    with pytest.raises(LuftdatenConnectionError):
+        client = Luftdaten(SENSOR_ID)
+        await client.get_data()
+
+@pytest.mark.asyncio
+async def test_read_timeout(httpx_mock: HTTPXMock):
+    """Test if the connection is hitting the timeout during data reading."""
 
     def raise_timeout(request):
         """Set the timeout for the requests."""
@@ -19,6 +36,6 @@ def raise_timeout(request):
 
     httpx_mock.add_callback(raise_timeout)
 
-    with pytest.raises(httpx.ReadTimeout):
+    with pytest.raises(LuftdatenConnectionError):
         client = Luftdaten(SENSOR_ID)
         await client.get_data()