diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 621b4dd299b..8fdc0143700 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -183,14 +183,27 @@ async def async_test_still( except ( TimeoutError, RequestError, - HTTPStatusError, TimeoutException, ) as err: _LOGGER.error("Error getting camera image from %s: %s", url, type(err).__name__) return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + except HTTPStatusError as err: + _LOGGER.error( + "Error getting camera image from %s: %s %s", + url, + type(err).__name__, + err.response.text, + ) + if err.response.status_code in [401, 403]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_auth"}, None + if err.response.status_code in [404]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_not_found"}, None + if err.response.status_code in [500, 503]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_server_error"}, None + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None if not image: - return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + return {CONF_STILL_IMAGE_URL: "unable_still_load_no_image"}, None fmt = get_image_type(image) _LOGGER.debug( "Still image at '%s' detected format: %s", diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index a1519fa0f48..991a36d49cc 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -5,6 +5,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_exists": "A camera with these URL settings already exists.", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "unable_still_load_auth": "Unable to load valid image from still image URL: The camera may require a user name and password, or they are not correct.", + "unable_still_load_not_found": "Unable to load valid image from still image URL: The URL was not found on the server.", + "unable_still_load_server_error": "Unable to load valid image from still image URL: The camera replied with a server error.", + "unable_still_load_no_image": "Unable to load valid image from still image URL: No image was returned.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", "malformed_url": "Malformed URL", @@ -73,6 +77,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_exists": "[%key:component::generic::config::error::already_exists%]", "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", + "unable_still_load_auth": "[%key:component::generic::config::error::unable_still_load_auth%]", + "unable_still_load_not_found": "[%key:component::generic::config::error::unable_still_load_not_found%]", + "unable_still_load_server_error": "[%key:component::generic::config::error::unable_still_load_server_error%]", + "unable_still_load_no_image": "[%key:component::generic::config::error::unable_still_load_no_image%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", "malformed_url": "[%key:component::generic::config::error::malformed_url%]", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 6ced5086282..7eee5f1bd9e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -449,12 +449,42 @@ async def test_form_still_and_stream_not_provided( @respx.mock -async def test_form_image_timeout( - hass: HomeAssistant, user_flow, mock_create_stream +@pytest.mark.parametrize( + ("side_effect", "expected_message"), + [ + (httpx.TimeoutException, {"still_image_url": "unable_still_load"}), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(401)), + {"still_image_url": "unable_still_load_auth"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(403)), + {"still_image_url": "unable_still_load_auth"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(404)), + {"still_image_url": "unable_still_load_not_found"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(500)), + {"still_image_url": "unable_still_load_server_error"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(503)), + {"still_image_url": "unable_still_load_server_error"}, + ), + ( # Errors without specific handler should show the general message. + httpx.HTTPStatusError("", request=None, response=httpx.Response(507)), + {"still_image_url": "unable_still_load"}, + ), + ], +) +async def test_form_image_http_exceptions( + side_effect, expected_message, hass: HomeAssistant, user_flow, mock_create_stream ) -> None: - """Test we handle invalid image timeout.""" + """Test we handle image http exceptions.""" respx.get("http://127.0.0.1/testurl/1").side_effect = [ - httpx.TimeoutException, + side_effect, ] with mock_create_stream: @@ -465,7 +495,7 @@ async def test_form_image_timeout( await hass.async_block_till_done() assert result2["type"] == "form" - assert result2["errors"] == {"still_image_url": "unable_still_load"} + assert result2["errors"] == expected_message @respx.mock @@ -499,7 +529,7 @@ async def test_form_stream_invalidimage2( await hass.async_block_till_done() assert result2["type"] == "form" - assert result2["errors"] == {"still_image_url": "unable_still_load"} + assert result2["errors"] == {"still_image_url": "unable_still_load_no_image"} @respx.mock