diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 5be1dbc1a48..2bcdaddf739 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -5,7 +5,7 @@ import shutil from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from go2rtc_client import Go2RtcRestClient -from go2rtc_client.exceptions import Go2RtcClientError +from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.ws import ( Go2RtcWsClient, ReceiveMessages, @@ -114,7 +114,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: server = Server( hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False) ) - await server.start() + try: + await server.start() + except Exception: # noqa: BLE001 + _LOGGER.warning("Could not start go2rtc server", exc_info=True) + return False async def on_stop(event: Event) -> None: await server.stop() @@ -143,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Validate the server URL try: client = Go2RtcRestClient(async_get_clientsession(hass), url) - await client.streams.list() + await client.validate_server_version() except Go2RtcClientError as err: if isinstance(err.__cause__, _RETRYABLE_ERRORS): raise ConfigEntryNotReady( @@ -151,6 +155,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from err _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False + except Go2RtcVersionError as err: + raise ConfigEntryNotReady( + f"The go2rtc server version is not supported, {err}" + ) from err except Exception as err: # noqa: BLE001 _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index b2aa19d5275..eff067416b3 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -112,6 +112,10 @@ class Server: await self._stop() raise Go2RTCServerStartError from err + # Check the server version + client = Go2RtcRestClient(async_get_clientsession(self._hass), DEFAULT_URL) + await client.validate_server_version() + async def _log_output(self, process: asyncio.subprocess.Process) -> None: """Log the output of the process.""" assert process.stdout is not None @@ -174,7 +178,7 @@ class Server: _LOGGER.debug("Monitoring go2rtc API") try: while True: - await client.streams.list() + await client.validate_server_version() await asyncio.sleep(10) except Exception as err: _LOGGER.debug("go2rtc API did not reply", exc_info=True) diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index 87c68989fd2..42b363b2324 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -23,6 +23,7 @@ def rest_client() -> Generator[AsyncMock]: client = mock_client.return_value client.streams = streams = Mock(spec_set=_StreamClient) streams.list.return_value = {} + client.validate_server_version = AsyncMock() client.webrtc = Mock(spec_set=_WebRTCClient) yield client diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 847de248aaf..21d4d0a047e 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from go2rtc_client import Stream -from go2rtc_client.exceptions import Go2RtcClientError +from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError from go2rtc_client.models import Producer from go2rtc_client.ws import ( ReceiveMessages, @@ -494,6 +494,8 @@ ERR_CONNECT = "Could not connect to go2rtc instance" ERR_CONNECT_RETRY = ( "Could not connect to go2rtc instance on http://localhost:1984/; Retrying" ) +ERR_START_SERVER = "Could not start go2rtc server" +ERR_UNSUPPORTED_VERSION = "The go2rtc server version is not supported" _INVALID_CONFIG = "Invalid config for 'go2rtc': " ERR_INVALID_URL = _INVALID_CONFIG + "invalid url" ERR_EXCLUSIVE = _INVALID_CONFIG + DEBUG_UI_URL_MESSAGE @@ -526,8 +528,10 @@ async def test_non_user_setup_with_error( ("config", "go2rtc_binary", "is_docker_env", "expected_log_message"), [ ({DEFAULT_CONFIG_DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND), + ({DEFAULT_CONFIG_DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_START_SERVER), ({DOMAIN: {}}, None, False, ERR_URL_REQUIRED), ({DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND), + ({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_START_SERVER), ({DOMAIN: {CONF_URL: "invalid"}}, None, True, ERR_INVALID_URL), ( {DOMAIN: {CONF_URL: "http://localhost:1984", CONF_DEBUG_UI: True}}, @@ -559,8 +563,6 @@ async def test_setup_with_setup_error( @pytest.mark.parametrize( ("config", "go2rtc_binary", "is_docker_env", "expected_log_message"), [ - ({DEFAULT_CONFIG_DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT), - ({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT), ({DOMAIN: {CONF_URL: "http://localhost:1984/"}}, None, True, ERR_CONNECT), ], ) @@ -584,7 +586,7 @@ async def test_setup_with_setup_entry_error( assert expected_log_message in caplog.text -@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}]) +@pytest.mark.parametrize("config", [{DOMAIN: {CONF_URL: "http://localhost:1984/"}}]) @pytest.mark.parametrize( ("cause", "expected_config_entry_state", "expected_log_message"), [ @@ -598,7 +600,7 @@ async def test_setup_with_setup_entry_error( @pytest.mark.usefixtures( "mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server" ) -async def test_setup_with_retryable_setup_entry_error( +async def test_setup_with_retryable_setup_entry_error_custom_server( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, rest_client: AsyncMock, @@ -610,7 +612,78 @@ async def test_setup_with_retryable_setup_entry_error( """Test setup integration entry fails.""" go2rtc_error = Go2RtcClientError() go2rtc_error.__cause__ = cause - rest_client.streams.list.side_effect = go2rtc_error + rest_client.validate_server_version.side_effect = go2rtc_error + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == expected_config_entry_state + assert expected_log_message in caplog.text + + +@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}]) +@pytest.mark.parametrize( + ("cause", "expected_config_entry_state", "expected_log_message"), + [ + (ClientConnectionError(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER), + (ServerConnectionError(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER), + (None, ConfigEntryState.NOT_LOADED, ERR_START_SERVER), + (Exception(), ConfigEntryState.NOT_LOADED, ERR_START_SERVER), + ], +) +@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) +@pytest.mark.usefixtures( + "mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server" +) +async def test_setup_with_retryable_setup_entry_error_default_server( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + rest_client: AsyncMock, + has_go2rtc_entry: bool, + config: ConfigType, + cause: Exception, + expected_config_entry_state: ConfigEntryState, + expected_log_message: str, +) -> None: + """Test setup integration entry fails.""" + go2rtc_error = Go2RtcClientError() + go2rtc_error.__cause__ = cause + rest_client.validate_server_version.side_effect = go2rtc_error + assert not await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done(wait_background_tasks=True) + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == has_go2rtc_entry + for config_entry in config_entries: + assert config_entry.state == expected_config_entry_state + assert expected_log_message in caplog.text + + +@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}]) +@pytest.mark.parametrize( + ("go2rtc_error", "expected_config_entry_state", "expected_log_message"), + [ + ( + Go2RtcVersionError("1.9.4", "1.9.5", "2.0.0"), + ConfigEntryState.SETUP_RETRY, + ERR_UNSUPPORTED_VERSION, + ), + ], +) +@pytest.mark.parametrize("has_go2rtc_entry", [True, False]) +@pytest.mark.usefixtures( + "mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server" +) +async def test_setup_with_version_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + rest_client: AsyncMock, + config: ConfigType, + go2rtc_error: Exception, + expected_config_entry_state: ConfigEntryState, + expected_log_message: str, +) -> None: + """Test setup integration entry fails.""" + rest_client.validate_server_version.side_effect = [None, go2rtc_error] assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done(wait_background_tasks=True) config_entries = hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/go2rtc/test_server.py b/tests/components/go2rtc/test_server.py index 1410fbeb6c3..fedf155baf5 100644 --- a/tests/components/go2rtc/test_server.py +++ b/tests/components/go2rtc/test_server.py @@ -47,6 +47,7 @@ def mock_tempfile() -> Generator[Mock]: ) async def test_server_run_success( mock_create_subprocess: AsyncMock, + rest_client: AsyncMock, server_stdout: list[str], server: Server, caplog: pytest.LogCaptureFixture, @@ -95,7 +96,7 @@ webrtc: @pytest.mark.usefixtures("mock_tempfile") async def test_server_timeout_on_stop( - mock_create_subprocess: MagicMock, server: Server + mock_create_subprocess: MagicMock, rest_client: AsyncMock, server: Server ) -> None: """Test server run where the process takes too long to terminate.""" # Start server thread