diff --git a/aiocomfoconnect/bridge.py b/aiocomfoconnect/bridge.py index cb0c4cd..d70351c 100644 --- a/aiocomfoconnect/bridge.py +++ b/aiocomfoconnect/bridge.py @@ -16,6 +16,7 @@ from .const import VENTILATION_UNIT_PRODUCT_IDS from .exceptions import ( AioComfoConnectNotConnected, + AioComfoConnectNotReachable, AioComfoConnectTimeout, ComfoConnectBadRequest, ComfoConnectError, @@ -189,8 +190,13 @@ async def _open_connection(self, uuid: str): try: self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self.host, self.PORT), TIMEOUT) except asyncio.TimeoutError as exc: + # Keep this before OSError, since TimeoutError is a subclass of it. _LOGGER.warning("Timeout while connecting to bridge %s", self.host) raise AioComfoConnectTimeout("Timeout while connecting to bridge") from exc + except OSError as exc: + # The bridge refused the connection, is gone from the network, or its hostname doesn't resolve. + _LOGGER.warning("Could not connect to bridge %s: %s", self.host, exc) + raise AioComfoConnectNotReachable(f"Could not connect to bridge: {exc}") from exc self._reference = itertools.count(1) self._local_uuid = uuid diff --git a/aiocomfoconnect/comfoconnect.py b/aiocomfoconnect/comfoconnect.py index 30715d1..219b510 100644 --- a/aiocomfoconnect/comfoconnect.py +++ b/aiocomfoconnect/comfoconnect.py @@ -31,6 +31,7 @@ ) from aiocomfoconnect.exceptions import ( AioComfoConnectNotConnected, + AioComfoConnectNotReachable, AioComfoConnectTimeout, ComfoConnectNotAllowed, VentilationUnitNotFoundException, @@ -180,6 +181,10 @@ async def _reconnect_loop(self, uuid: str): _LOGGER.warning("Connection timeout, retrying in 5 seconds...") await asyncio.sleep(5) + except AioComfoConnectNotReachable as exc: + _LOGGER.warning("%s. Retrying in 5 seconds...", exc) + await asyncio.sleep(5) + except VentilationUnitNotFoundException as exc: _LOGGER.warning("%s, retrying in 5 seconds...", exc) await asyncio.sleep(5) diff --git a/aiocomfoconnect/exceptions.py b/aiocomfoconnect/exceptions.py index cc03bb2..99a035c 100644 --- a/aiocomfoconnect/exceptions.py +++ b/aiocomfoconnect/exceptions.py @@ -50,6 +50,10 @@ class AioComfoConnectTimeout(Exception): """An error occurred because the bridge didn't reply in time.""" +class AioComfoConnectNotReachable(Exception): + """An error occurred because the bridge could not be reached, for example when it moved to another address.""" + + class BridgeNotFoundException(Exception): """Exception raised when no bridge is found.""" diff --git a/tests/test_bridge.py b/tests/test_bridge.py index 6591af6..965882c 100644 --- a/tests/test_bridge.py +++ b/tests/test_bridge.py @@ -1,6 +1,7 @@ """Tests for the Bridge class.""" import asyncio +import socket from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -14,6 +15,7 @@ from aiocomfoconnect.exceptions import ( AioComfoConnectNotConnected, + AioComfoConnectNotReachable, AioComfoConnectTimeout, ComfoConnectNotAllowed, VentilationUnitNotFoundException, @@ -151,6 +153,26 @@ async def timeout_coro(*args, **kwargs): await bridge.connect(LOCAL_UUID) assert not bridge.is_connected() + @pytest.mark.asyncio + @pytest.mark.parametrize( + "error", + [ + OSError(113, "No route to host"), + ConnectionRefusedError(111, "Connection refused"), + socket.gaierror("Name or service not known"), + ], + ) + async def test_connect_not_reachable(self, bridge, error): + """Test that a bridge we can't reach doesn't raise a bare OSError.""" + + async def error_coro(*args, **kwargs): + raise error + + with patch("asyncio.open_connection", side_effect=error_coro): + with pytest.raises(AioComfoConnectNotReachable, match="Could not connect to bridge"): + await bridge.connect(LOCAL_UUID) + assert not bridge.is_connected() + @pytest.mark.asyncio async def test_connect_already_connected(self, bridge, mock_connection): """Test connecting when already connected."""