diff --git a/.coveragerc b/.coveragerc index b267f6967ef..9e1ec415006 100644 --- a/.coveragerc +++ b/.coveragerc @@ -578,7 +578,6 @@ omit = homeassistant/components/nest/binary_sensor.py homeassistant/components/nest/camera.py homeassistant/components/nest/camera_legacy.py - homeassistant/components/nest/camera_sdm.py homeassistant/components/nest/climate.py homeassistant/components/nest/climate_legacy.py homeassistant/components/nest/climate_sdm.py diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index cec35eeca29..37bd2fed8a6 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -95,9 +95,10 @@ class NestCamera(Camera): @property def supported_features(self): """Flag supported features.""" + supported_features = 0 if CameraLiveStreamTrait.NAME in self._device.traits: - return SUPPORT_STREAM - return 0 + supported_features |= SUPPORT_STREAM + return supported_features async def stream_source(self): """Return the source of the stream.""" @@ -131,7 +132,6 @@ class NestCamera(Camera): if not self._stream: return _LOGGER.debug("Extending stream url") - self._stream_refresh_unsub = None try: self._stream = await self._stream.extend_rtsp_stream() except GoogleNestException as err: diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 4a018305bcf..69b413ba51a 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -9,9 +9,11 @@ import datetime import aiohttp from google_nest_sdm.device import Device +import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -140,6 +142,36 @@ async def test_camera_stream(hass, auth): assert image.content == b"image bytes" +async def test_camera_stream_missing_trait(hass, auth): + """Test fetching a video stream when not supported by the API.""" + traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 800, + "height": 600, + } + }, + } + + await async_setup_camera(hass, 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 + + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source is None + + # Currently on support getting the image from a live stream + with pytest.raises(HomeAssistantError): + image = await camera.async_get_image(hass, "camera.my_camera") + assert image is None + + async def test_refresh_expired_stream_token(hass, auth): """Test a camera stream expiration and refresh.""" now = utcnow() @@ -220,6 +252,59 @@ async def test_refresh_expired_stream_token(hass, auth): assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" +async def test_stream_response_already_expired(hass, auth): + """Test a API response returning an expired stream url.""" + now = utcnow() + stream_1_expiration = now + datetime.timedelta(seconds=-90) + stream_2_expiration = now + datetime.timedelta(seconds=+90) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" + }, + "streamExtensionToken": "g.1.extensionToken", + "streamToken": "g.1.streamingToken", + "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), + }, + } + ), + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" + }, + "streamExtensionToken": "g.2.extensionToken", + "streamToken": "g.2.streamingToken", + "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), + }, + } + ), + ] + 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 + + # The stream is expired, but we return it anyway + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" + + await fire_alarm(hass, now) + + # Second attempt sees that the stream is expired and refreshes + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" now = utcnow()