From 3d33cad655beae4908881a6b4b4fc9bd0ff07572 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Oct 2021 10:46:18 -0700 Subject: [PATCH] Improve nest error handling for websocket streams (#57885) --- homeassistant/components/nest/camera_sdm.py | 12 ++- tests/components/nest/camera_sdm_test.py | 100 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index b610d328b42..5620653819f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -24,7 +24,7 @@ from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_W from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -136,7 +136,10 @@ class NestCamera(Camera): trait = self._device.traits[CameraLiveStreamTrait.NAME] if not self._stream: _LOGGER.debug("Fetching stream url") - self._stream = await trait.generate_rtsp_stream() + try: + self._stream = await trait.generate_rtsp_stream() + except GoogleNestException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err self._schedule_stream_refresh() assert self._stream if self._stream.expires_at < utcnow(): @@ -271,5 +274,8 @@ class NestCamera(Camera): async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str: """Return the source of the stream.""" trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] - stream = await trait.generate_web_rtc_stream(offer_sdp) + try: + stream = await trait.generate_web_rtc_stream(offer_sdp) + except GoogleNestException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err return stream.answer_sdp diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index a6c7ad64605..08797fb3c6a 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -200,6 +200,61 @@ async def test_camera_stream(hass, auth): assert image.content == IMAGE_BYTES_FROM_STREAM +async def test_camera_ws_stream(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [make_stream_url_response()] + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + with patch("homeassistant.components.camera.create_stream") as mock_stream: + mock_stream().endpoint_url.return_value = "http://home.assistant/playlist.m3u8" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "camera/stream", + "entity_id": "camera.my_camera", + } + ) + msg = await client.receive_json() + + assert msg["id"] == 2 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"]["url"] == "http://home.assistant/playlist.m3u8" + + +async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [aiohttp.web.Response(status=400)] + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 3, + "type": "camera/stream", + "entity_id": "camera.my_camera", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 3 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "start_stream_failed" + assert msg["error"]["message"].startswith("Nest API error") + + async def test_camera_stream_missing_trait(hass, auth): """Test fetching a video stream when not supported by the API.""" traits = { @@ -686,3 +741,48 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): assert msg["type"] == TYPE_RESULT assert not msg["success"] assert msg["error"]["code"] == "web_rtc_offer_failed" + assert msg["error"]["message"].startswith("Camera does not support WebRTC") + + +async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [ + aiohttp.web.Response(status=400), + ] + device_traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC"], + }, + } + await async_setup_camera(hass, device_traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "web_rtc_offer_failed" + assert msg["error"]["message"].startswith("Nest API error")