mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Revert "Add an entity service for saving nest event related snapshots" (#60632)
This commit is contained in:
parent
b8a1899d48
commit
a84b12abe7
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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.event import ImageEventBase
|
||||||
from google_nest_sdm.exceptions import GoogleNestException
|
from google_nest_sdm.exceptions import GoogleNestException
|
||||||
from haffmpeg.tools import IMAGE_JPEG
|
from haffmpeg.tools import IMAGE_JPEG
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||||
from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
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 import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
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
|
from .device_info import NestDeviceInfo
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -67,17 +64,6 @@ async def async_setup_sdm_entry(
|
|||||||
entities.append(NestCamera(device))
|
entities.append(NestCamera(device))
|
||||||
async_add_entities(entities)
|
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):
|
class NestCamera(Camera):
|
||||||
"""Devices that support cameras."""
|
"""Devices that support cameras."""
|
||||||
@ -306,33 +292,3 @@ class NestCamera(Camera):
|
|||||||
except GoogleNestException as err:
|
except GoogleNestException as err:
|
||||||
raise HomeAssistantError(f"Nest API error: {err}") from err
|
raise HomeAssistantError(f"Nest API error: {err}") from err
|
||||||
return stream.answer_sdp
|
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
|
|
||||||
|
@ -22,5 +22,3 @@ SDM_SCOPES = [
|
|||||||
]
|
]
|
||||||
API_URL = "https://smartdevicemanagement.googleapis.com/v1"
|
API_URL = "https://smartdevicemanagement.googleapis.com/v1"
|
||||||
OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
|
OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
|
||||||
SERVICE_SNAPSHOT_EVENT = "snapshot_event"
|
|
||||||
|
@ -1,30 +1,8 @@
|
|||||||
# Describes the format for available Nest services
|
# 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:
|
set_away_mode:
|
||||||
name: 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:
|
fields:
|
||||||
away_mode:
|
away_mode:
|
||||||
name: Away mode
|
name: Away mode
|
||||||
@ -44,7 +22,7 @@ set_away_mode:
|
|||||||
|
|
||||||
set_eta:
|
set_eta:
|
||||||
name: Set estimated time of arrival
|
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:
|
fields:
|
||||||
eta:
|
eta:
|
||||||
name: ETA
|
name: ETA
|
||||||
@ -73,7 +51,7 @@ set_eta:
|
|||||||
|
|
||||||
cancel_eta:
|
cancel_eta:
|
||||||
name: 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:
|
fields:
|
||||||
trip_id:
|
trip_id:
|
||||||
name: Trip ID
|
name: Trip ID
|
||||||
|
@ -7,8 +7,7 @@ pubsub subscriber.
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import os
|
from unittest.mock import patch
|
||||||
from unittest.mock import mock_open, patch
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from google_nest_sdm.device import Device
|
from google_nest_sdm.device import Device
|
||||||
@ -22,9 +21,7 @@ from homeassistant.components.camera import (
|
|||||||
STREAM_TYPE_HLS,
|
STREAM_TYPE_HLS,
|
||||||
STREAM_TYPE_WEB_RTC,
|
STREAM_TYPE_WEB_RTC,
|
||||||
)
|
)
|
||||||
from homeassistant.components.nest.const import SERVICE_SNAPSHOT_EVENT
|
|
||||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
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):
|
async def test_no_devices(hass):
|
||||||
"""Test configuration that returns no devices."""
|
"""Test configuration that returns no devices."""
|
||||||
await async_setup_camera(hass)
|
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):
|
async def test_camera_image_from_event_not_supported(hass, auth):
|
||||||
"""Test fallback to stream image when event images are not supported."""
|
"""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()
|
traits = DEVICE_TRAITS.copy()
|
||||||
del traits["sdm.devices.traits.CameraEventImage"]
|
del traits["sdm.devices.traits.CameraEventImage"]
|
||||||
subscriber = await async_setup_camera(hass, traits, auth=auth)
|
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["type"] == TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert msg["result"]["answer"] == "v=0\r\ns=-\r\n"
|
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)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user