diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 181b3321c53..68a4ce52511 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -316,6 +316,15 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Verify there is an H264 profile media_service = device.create_media_service() profiles = await media_service.GetProfiles() + except AttributeError: # Likely an empty document or 404 from the wrong port + LOGGER.debug( + "%s: No ONVIF service found at %s:%s", + self.onvif_config[CONF_NAME], + self.onvif_config[CONF_HOST], + self.onvif_config[CONF_PORT], + exc_info=True, + ) + return {CONF_PORT: "no_onvif_service"}, {} except Fault as err: stringified_error = stringify_onvif_error(err) description_placeholders = {"error": stringified_error} diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 07f2e6fb7ac..55413e4bf6c 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -11,6 +11,7 @@ "error": { "onvif_error": "Error setting up ONVIF device: {error}. Check logs for more information.", "auth_failed": "Could not authenticate: {error}", + "no_onvif_service": "No ONVIF service found. Check that the port number is correct.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "step": { diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 1dfcc85cd28..18de9839e1b 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -45,12 +45,14 @@ def setup_mock_onvif_camera( update_xaddrs_fail=False, no_profiles=False, auth_failure=False, + wrong_port=False, ): """Prepare mock onvif.ONVIFCamera.""" devicemgmt = MagicMock() device_info = MagicMock() device_info.SerialNumber = SERIAL_NUMBER if with_serial else None + devicemgmt.GetDeviceInformation = AsyncMock(return_value=device_info) interface = MagicMock() @@ -82,7 +84,9 @@ def setup_mock_onvif_camera( else: media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2]) - if auth_failure: + if wrong_port: + mock_onvif_camera.update_xaddrs = AsyncMock(side_effect=AttributeError) + elif auth_failure: mock_onvif_camera.update_xaddrs = AsyncMock( side_effect=Fault( "not authorized", subcodes=[MagicMock(text="NotAuthorized")] diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 805b5d85db8..21ef1cf3fc2 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -829,3 +829,82 @@ async def test_flow_manual_entry_updates_existing_user_password( assert entry.data[config_flow.CONF_USERNAME] == USERNAME assert entry.data[config_flow.CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: + """Test that we get a useful error with the wrong port.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device: + setup_mock_onvif_camera(mock_onvif_camera, wrong_port=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"auto": False}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "configure" + + with patch( + "homeassistant.components.onvif.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 0 + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "configure" + assert result["errors"] == {"port": "no_onvif_service"} + assert result["description_placeholders"] == {} + setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) + + with patch( + "homeassistant.components.onvif.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["title"] == f"{NAME} - {MAC}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }