mirror of
https://github.com/home-assistant/core.git
synced 2026-09-06 05:22:44 +01:00
Fix swallowed exceptions in action handlers for MJPEG IP Camera (#180353)
This commit is contained in:
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_web,
|
||||
async_get_clientsession,
|
||||
@@ -29,7 +30,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN, LOGGER
|
||||
from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN
|
||||
|
||||
TIMEOUT = 10
|
||||
BUFFER_SIZE = 102400
|
||||
@@ -155,14 +156,19 @@ class MjpegCamera(Camera):
|
||||
|
||||
return await response.read()
|
||||
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout getting camera image from %s", self.name)
|
||||
except TimeoutError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_getting_image",
|
||||
translation_placeholders={"name": str(self.name)},
|
||||
) from err
|
||||
|
||||
except aiohttp.ClientError as err:
|
||||
LOGGER.error("Error getting new camera image from %s: %s", self.name, err)
|
||||
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_getting_image",
|
||||
translation_placeholders={"name": str(self.name)},
|
||||
) from err
|
||||
|
||||
def _get_httpx_auth(self) -> httpx.Auth:
|
||||
"""Return a httpx auth object."""
|
||||
@@ -192,13 +198,19 @@ class MjpegCamera(Camera):
|
||||
stream.aiter_bytes(BUFFER_SIZE)
|
||||
)
|
||||
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout getting camera image from %s", self.name)
|
||||
except (TimeoutError, httpx.TimeoutException) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_getting_image",
|
||||
translation_placeholders={"name": str(self.name)},
|
||||
) from err
|
||||
|
||||
except httpx.HTTPError as err:
|
||||
LOGGER.error("Error getting new camera image from %s: %s", self.name, err)
|
||||
|
||||
return None
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="error_getting_image",
|
||||
translation_placeholders={"name": str(self.name)},
|
||||
) from err
|
||||
|
||||
async def _handle_async_mjpeg_digest_stream(
|
||||
self, request: web.Request
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"error_getting_image": {
|
||||
"message": "Error getting a new image from {name}."
|
||||
},
|
||||
"timeout_getting_image": {
|
||||
"message": "Timeout getting an image from {name}."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Test the MJPEG IP Camera camera platform."""
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from homeassistant.components.camera import async_get_image
|
||||
from homeassistant.components.mjpeg.const import (
|
||||
CONF_MJPEG_URL,
|
||||
CONF_STILL_IMAGE_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
ENTITY_CAMERA = "camera.my_mjpeg_camera"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "translation_key"),
|
||||
[
|
||||
pytest.param(TimeoutError, "timeout_getting_image", id="timeout"),
|
||||
pytest.param(aiohttp.ClientError, "error_getting_image", id="client_error"),
|
||||
],
|
||||
)
|
||||
async def test_camera_image_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
exception: type[Exception],
|
||||
translation_key: str,
|
||||
) -> None:
|
||||
"""Test that a failed still image request raises instead of returning nothing."""
|
||||
aioclient_mock.get("http://example.com/still", exc=exception)
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await async_get_image(hass, ENTITY_CAMERA)
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == translation_key
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_camera_image(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test that a still image is returned."""
|
||||
aioclient_mock.get("http://example.com/still", content=b"image_bytes")
|
||||
|
||||
image = await async_get_image(hass, ENTITY_CAMERA)
|
||||
|
||||
assert image.content == b"image_bytes"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_digest_config_entry() -> MockConfigEntry:
|
||||
"""Return a mocked config entry that uses digest authentication."""
|
||||
return MockConfigEntry(
|
||||
title="My MJPEG Camera",
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
CONF_MJPEG_URL: "https://example.com/mjpeg",
|
||||
CONF_PASSWORD: "supersecret",
|
||||
CONF_STILL_IMAGE_URL: "http://example.com/still",
|
||||
CONF_USERNAME: "frenck",
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "translation_key"),
|
||||
[
|
||||
pytest.param(TimeoutError, "timeout_getting_image", id="timeout"),
|
||||
pytest.param(
|
||||
httpx.TimeoutException, "timeout_getting_image", id="httpx_timeout"
|
||||
),
|
||||
pytest.param(httpx.HTTPError, "error_getting_image", id="http_error"),
|
||||
],
|
||||
)
|
||||
async def test_digest_camera_image_error(
|
||||
hass: HomeAssistant,
|
||||
mock_digest_config_entry: MockConfigEntry,
|
||||
exception: type[Exception],
|
||||
translation_key: str,
|
||||
) -> None:
|
||||
"""Test that a failed digest image request raises instead of returning nothing."""
|
||||
mock_digest_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_digest_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
respx.get("http://example.com/still").mock(side_effect=exception("boom"))
|
||||
respx.get("https://example.com/mjpeg").mock(side_effect=exception("boom"))
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await async_get_image(hass, ENTITY_CAMERA)
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == translation_key
|
||||
Reference in New Issue
Block a user