diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 8d14dfbf3ac09b..9d84ca07e60856 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -1,5 +1,6 @@ """The Anthem A/V Receivers integration.""" +import asyncio import logging import anthemav @@ -20,7 +21,13 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ANTHEMAV_UPDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN, MANUFACTURER +from .const import ( + ANTHEMAV_UPDATE_SIGNAL, + CONNECT_TIMEOUT_SECONDS, + DEVICE_TIMEOUT_SECONDS, + DOMAIN, + MANUFACTURER, +) type AnthemavConfigEntry = ConfigEntry[anthemav.Connection] @@ -39,14 +46,22 @@ def async_anthemav_update_callback(message: str) -> None: async_dispatcher_send(hass, f"{ANTHEMAV_UPDATE_SIGNAL}_{entry.entry_id}") try: - avr = await anthemav.Connection.create( - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - update_callback=async_anthemav_update_callback, - ) + # See CONNECT_TIMEOUT_SECONDS for why this needs a timeout. + async with asyncio.timeout(CONNECT_TIMEOUT_SECONDS): + avr = await anthemav.Connection.create( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + update_callback=async_anthemav_update_callback, + ) # Wait for the zones to be initialised based on the model await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + except TimeoutError as err: + # Raised only by the asyncio.timeout() above; the connection never completed. + raise ConfigEntryNotReady( + f"Timed out connecting to Anthem AVR at " + f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + ) from err except (OSError, DeviceError) as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index 8bcdd013a63e18..fcefd757f31117 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -7,3 +7,6 @@ DOMAIN = "anthemav" MANUFACTURER = "Anthem" DEVICE_TIMEOUT_SECONDS = 4.0 +# anthemav.Connection.create() retries internally and only returns once +# connected, so it never fails on its own when the receiver is unreachable. +CONNECT_TIMEOUT_SECONDS = 10.0 diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 27a32bacff5cfd..6b05f92253e32d 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,5 +1,6 @@ """Test the Anthem A/V Receivers config flow.""" +import asyncio from collections.abc import Callable from unittest.mock import ANY, AsyncMock, patch @@ -72,6 +73,30 @@ async def test_config_entry_not_ready_when_oserror( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_config_entry_not_ready_when_connect_hangs( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test setup fails fast (instead of hanging) when the AVR never connects.""" + + async def _hang(*args, **kwargs) -> None: + await asyncio.sleep(3600) + + with ( + patch( + "homeassistant.components.anthemav.CONNECT_TIMEOUT_SECONDS", + 0.01, + ), + patch( + "anthemav.Connection.create", + side_effect=_hang, + ), + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + async def test_anthemav_dispatcher_signal( hass: HomeAssistant, mock_connection_create: AsyncMock,