From 94ccd59123b4a837d67ffa6b2971a892d34392ce Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 Feb 2024 20:19:38 +0100 Subject: [PATCH] Fix generic camera error when template renders to an invalid URL (#109737) --- homeassistant/components/generic/camera.py | 7 +++++ tests/components/generic/test_camera.py | 30 +++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 902f5ebadde..cadc855ade6 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -8,6 +8,7 @@ import logging from typing import Any import httpx +import voluptuous as vol import yarl from homeassistant.components.camera import Camera, CameraEntityFeature @@ -140,6 +141,12 @@ class GenericCamera(Camera): _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err) return self._last_image + try: + vol.Schema(vol.Url())(url) + except vol.Invalid as err: + _LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err) + return self._last_image + if url == self._last_url and self._limit_refetch: return self._last_image diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index ba7f4d3d4a1..608b8666027 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -70,15 +70,20 @@ async def help_setup_mock_config_entry( @respx.mock async def test_fetching_url( - hass: HomeAssistant, hass_client: ClientSessionGenerator, fakeimgbytes_png + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + fakeimgbytes_png, + caplog: pytest.CaptureFixture, ) -> None: """Test that it fetches the given url.""" - respx.get("http://example.com").respond(stream=fakeimgbytes_png) + hass.states.async_set("sensor.temp", "http://example.com/0a") + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) + respx.get("http://example.com/1a").respond(stream=fakeimgbytes_png) options = { "name": "config_test", "platform": "generic", - "still_image_url": "http://example.com", + "still_image_url": "{{ states.sensor.temp.state }}", "username": "user", "password": "pass", "authentication": "basic", @@ -101,6 +106,25 @@ async def test_fetching_url( resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 2 + # If the template renders to an invalid URL we return the last image from cache + hass.states.async_set("sensor.temp", "invalid url") + + # sleep another .1 seconds to make cached image expire + await asyncio.sleep(0.1) + resp = await client.get("/api/camera_proxy/camera.config_test") + assert resp.status == HTTPStatus.OK + assert respx.calls.call_count == 2 + assert ( + "Invalid URL 'invalid url': expected a URL, returning last image" in caplog.text + ) + + # Restore a valid URL + hass.states.async_set("sensor.temp", "http://example.com/1a") + await asyncio.sleep(0.1) + resp = await client.get("/api/camera_proxy/camera.config_test") + assert resp.status == HTTPStatus.OK + assert respx.calls.call_count == 3 + @respx.mock async def test_image_caching(