diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 562b55ba652..5385eb42b26 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable import datetime import logging -import os from pathlib import Path from typing import Any @@ -20,7 +19,6 @@ from google_nest_sdm.device import Device from google_nest_sdm.event import ImageEventBase from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG -import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC @@ -28,13 +26,12 @@ from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN, SERVICE_SNAPSHOT_EVENT +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -67,17 +64,6 @@ async def async_setup_sdm_entry( entities.append(NestCamera(device)) async_add_entities(entities) - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_SNAPSHOT_EVENT, - { - vol.Required("nest_event_id"): cv.string, - vol.Required("filename"): cv.string, - }, - "_async_snapshot_event", - ) - class NestCamera(Camera): """Devices that support cameras.""" @@ -306,33 +292,3 @@ class NestCamera(Camera): except GoogleNestException as err: raise HomeAssistantError(f"Nest API error: {err}") from err return stream.answer_sdp - - async def _async_snapshot_event(self, nest_event_id: str, filename: str) -> None: - """Save media for a Nest event, based on `camera.snapshot`.""" - _LOGGER.debug("Taking snapshot for event id '%s'", nest_event_id) - if not self.hass.config.is_allowed_path(filename): - raise HomeAssistantError("No access to write snapshot '%s'" % filename) - # Fetch media associated with the event - if not (trait := self._device.traits.get(CameraEventImageTrait.NAME)): - raise HomeAssistantError("Camera does not support event image snapshots") - try: - event_image = await trait.generate_image(nest_event_id) - except GoogleNestException as err: - raise HomeAssistantError("Unable to create event snapshot") from err - try: - image = await event_image.contents() - except GoogleNestException as err: - raise HomeAssistantError("Unable to fetch event snapshot") from err - - _LOGGER.debug("Writing event snapshot to '%s'", filename) - - def _write_image() -> None: - """Executor helper to write image.""" - os.makedirs(os.path.dirname(filename), exist_ok=True) - with open(filename, "wb") as img_file: - img_file.write(image) - - try: - await self.hass.async_add_executor_job(_write_image) - except OSError as err: - raise HomeAssistantError("Failed to write snapshot image") from err diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index e98d563c574..a92a48bfd6c 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -22,5 +22,3 @@ SDM_SCOPES = [ ] API_URL = "https://smartdevicemanagement.googleapis.com/v1" OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" - -SERVICE_SNAPSHOT_EVENT = "snapshot_event" diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index d432d2a3859..98aacf60524 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,30 +1,8 @@ # Describes the format for available Nest services -snapshot_event: - name: Take event snapshot - description: Take a snapshot from a camera for an event. - target: - entity: - integration: nest - domain: camera - fields: - nest_event_id: - name: Nest Event Id - description: The nest_event_id from the event to snapshot. Can be populated by an automation trigger for a 'nest_event' with 'data_template'. - required: true - selector: - text: - filename: - name: Filename - description: A filename where the snapshot for the event is written. - required: true - example: "/tmp/snapshot_my_camera.jpg" - selector: - text: - set_away_mode: name: Set away mode - description: Set the away mode for a Nest structure. For Legacy API. + description: Set the away mode for a Nest structure. fields: away_mode: name: Away mode @@ -44,7 +22,7 @@ set_away_mode: set_eta: name: Set estimated time of arrival - description: Set or update the estimated time of arrival window for a Nest structure. For Legacy API. + description: Set or update the estimated time of arrival window for a Nest structure. fields: eta: name: ETA @@ -73,7 +51,7 @@ set_eta: cancel_eta: name: Cancel ETA - description: Cancel an existing estimated time of arrival window for a Nest structure. For Legacy API. + description: Cancel an existing estimated time of arrival window for a Nest structure. fields: trip_id: name: Trip ID diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 07184556819..1ac1b4ca6f9 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -7,8 +7,7 @@ pubsub subscriber. import datetime from http import HTTPStatus -import os -from unittest.mock import mock_open, patch +from unittest.mock import patch import aiohttp from google_nest_sdm.device import Device @@ -22,9 +21,7 @@ from homeassistant.components.camera import ( STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, ) -from homeassistant.components.nest.const import SERVICE_SNAPSHOT_EVENT from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -153,20 +150,6 @@ async def async_get_image(hass, width=None, height=None): ) -async def async_call_service_event_snapshot(hass, filename): - """Call the event snapshot service.""" - return await hass.services.async_call( - DOMAIN, - SERVICE_SNAPSHOT_EVENT, - { - ATTR_ENTITY_ID: "camera.my_camera", - "nest_event_id": "some-event-id", - "filename": filename, - }, - blocking=True, - ) - - async def test_no_devices(hass): """Test configuration that returns no devices.""" await async_setup_camera(hass) @@ -536,7 +519,7 @@ async def test_camera_image_from_last_event(hass, auth): async def test_camera_image_from_event_not_supported(hass, auth): """Test fallback to stream image when event images are not supported.""" - # Create a device that does not support the CameraEventImage trait + # Create a device that does not support the CameraEventImgae trait traits = DEVICE_TRAITS.copy() del traits["sdm.devices.traits.CameraEventImage"] subscriber = await async_setup_camera(hass, traits, auth=auth) @@ -882,135 +865,3 @@ async def test_camera_multiple_streams(hass, auth, hass_ws_client): assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" - - -async def test_service_snapshot_event_image(hass, auth, tmpdir): - """Test calling the snapshot_event service.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True): - assert await async_call_service_event_snapshot(hass, filename) - - assert os.path.exists(filename) - with open(filename, "rb") as f: - contents = f.read() - assert contents == IMAGE_BYTES_FROM_EVENT - - -async def test_service_snapshot_no_access_to_filename(hass, auth, tmpdir): - """Test calling the snapshot_event service with a disallowed file path.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object( - hass.config, "is_allowed_path", return_value=False - ), pytest.raises(HomeAssistantError, match=r"No access.*"): - assert await async_call_service_event_snapshot(hass, filename) - - assert not os.path.exists(filename) - - -async def test_camera_snapshot_from_event_not_supported(hass, auth, tmpdir): - """Test a camera that does not support snapshots.""" - # Create a device that does not support the CameraEventImage trait - traits = DEVICE_TRAITS.copy() - del traits["sdm.devices.traits.CameraEventImage"] - await async_setup_camera(hass, traits, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Camera does not support.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_generate_url_failure(hass, auth, tmpdir): - """Test failure while creating a snapshot url.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Unable to create.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_image_fetch_invalid(hass, auth, tmpdir): - """Test failure when fetching an image snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), pytest.raises( - HomeAssistantError, match=r"Unable to fetch.*" - ): - await async_call_service_event_snapshot(hass, filename) - assert not os.path.exists(filename) - - -async def test_service_snapshot_event_image_create_directory(hass, auth, tmpdir): - """Test creating the directory when writing the snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/path/does/not/exist/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True): - assert await async_call_service_event_snapshot(hass, filename) - - assert os.path.exists(filename) - with open(filename, "rb") as f: - contents = f.read() - assert contents == IMAGE_BYTES_FROM_EVENT - - -async def test_service_snapshot_event_write_failure(hass, auth, tmpdir): - """Test a failure when writing the snapshot.""" - await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("camera.my_camera") - - auth.responses = [ - aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), - aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), - ] - - filename = f"{tmpdir}/snapshot.jpg" - with patch.object(hass.config, "is_allowed_path", return_value=True), patch( - "homeassistant.components.nest.camera_sdm.open", mock_open(), create=True - ) as mocked_open, pytest.raises(HomeAssistantError, match=r"Failed to write.*"): - mocked_open.side_effect = IOError() - assert await async_call_service_event_snapshot(hass, filename) - - assert not os.path.exists(filename)