Update nest events to include attachment image and video urls (#124554)

pull/124590/head
Allen Porter 2024-08-25 12:24:06 -07:00 committed by GitHub
parent 3304e27fa3
commit 18212052a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 142 additions and 4 deletions

View File

@ -20,6 +20,7 @@ from google_nest_sdm.exceptions import (
DecodeException, DecodeException,
SubscriberException, SubscriberException,
) )
from google_nest_sdm.traits import TraitType
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.auth.permissions.const import POLICY_READ
@ -65,6 +66,8 @@ from .const import (
) )
from .events import EVENT_NAME_MAP, NEST_EVENT from .events import EVENT_NAME_MAP, NEST_EVENT
from .media_source import ( from .media_source import (
EVENT_MEDIA_API_URL_FORMAT,
EVENT_THUMBNAIL_URL_FORMAT,
async_get_media_event_store, async_get_media_event_store,
async_get_media_source_devices, async_get_media_source_devices,
async_get_transcoder, async_get_transcoder,
@ -136,11 +139,15 @@ class SignalUpdateCallback:
"""An EventCallback invoked when new events arrive from subscriber.""" """An EventCallback invoked when new events arrive from subscriber."""
def __init__( def __init__(
self, hass: HomeAssistant, config_reload_cb: Callable[[], Awaitable[None]] self,
hass: HomeAssistant,
config_reload_cb: Callable[[], Awaitable[None]],
config_entry_id: str,
) -> None: ) -> None:
"""Initialize EventCallback.""" """Initialize EventCallback."""
self._hass = hass self._hass = hass
self._config_reload_cb = config_reload_cb self._config_reload_cb = config_reload_cb
self._config_entry_id = config_entry_id
async def async_handle_event(self, event_message: EventMessage) -> None: async def async_handle_event(self, event_message: EventMessage) -> None:
"""Process an incoming EventMessage.""" """Process an incoming EventMessage."""
@ -162,16 +169,36 @@ class SignalUpdateCallback:
for api_event_type, image_event in events.items(): for api_event_type, image_event in events.items():
if not (event_type := EVENT_NAME_MAP.get(api_event_type)): if not (event_type := EVENT_NAME_MAP.get(api_event_type)):
continue continue
nest_event_id = image_event.event_token
attachment = {
"image": EVENT_THUMBNAIL_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
),
}
if self._supports_clip(device_id):
attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
)
message = { message = {
"device_id": device_entry.id, "device_id": device_entry.id,
"type": event_type, "type": event_type,
"timestamp": event_message.timestamp, "timestamp": event_message.timestamp,
"nest_event_id": image_event.event_token, "nest_event_id": nest_event_id,
"attachment": attachment,
} }
if image_event.zones: if image_event.zones:
message["zones"] = image_event.zones message["zones"] = image_event.zones
self._hass.bus.async_fire(NEST_EVENT, message) self._hass.bus.async_fire(NEST_EVENT, message)
def _supports_clip(self, device_id: str) -> bool:
if not (
device_manager := self._hass.data[DOMAIN]
.get(self._config_entry_id, {})
.get(DATA_DEVICE_MANAGER)
) or not (device := device_manager.devices.get(device_id)):
return False
return TraitType.CAMERA_CLIP_PREVIEW in device.traits
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Nest from a config entry with dispatch between old/new flows.""" """Set up Nest from a config entry with dispatch between old/new flows."""
@ -197,7 +224,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_config_reload() -> None: async def async_config_reload() -> None:
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
update_callback = SignalUpdateCallback(hass, async_config_reload) update_callback = SignalUpdateCallback(hass, async_config_reload, entry.entry_id)
subscriber.set_update_callback(update_callback.async_handle_event) subscriber.set_update_callback(update_callback.async_handle_event)
try: try:
await subscriber.start_async() await subscriber.start_async()

View File

@ -186,6 +186,8 @@ async def test_event(
"type": expected_type, "type": expected_type,
"timestamp": event_time, "timestamp": event_time,
} }
assert "image" in events[0].data["attachment"]
assert "video" not in events[0].data["attachment"]
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -344,6 +346,8 @@ async def test_doorbell_event_thread(
"type": "camera_motion", "type": "camera_motion",
"timestamp": timestamp1.replace(microsecond=0), "timestamp": timestamp1.replace(microsecond=0),
} }
assert "image" in events[0].data["attachment"]
assert "video" in events[0].data["attachment"]
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -74,7 +74,6 @@ GENERATE_IMAGE_URL_RESPONSE = {
} }
IMAGE_BYTES_FROM_EVENT = b"test url image bytes" IMAGE_BYTES_FROM_EVENT = b"test url image bytes"
IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"}
NEST_EVENT = "nest_event"
def frame_image_data(frame_i, total_frames): def frame_image_data(frame_i, total_frames):
@ -1461,3 +1460,111 @@ async def test_camera_image_resize(
assert browse.title == "Front: Recent Events" assert browse.title == "Front: Recent Events"
assert not browse.thumbnail assert not browse.thumbnail
assert len(browse.children) == 1 assert len(browse.children) == 1
async def test_event_media_attachment(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
subscriber,
auth,
setup_platform,
) -> None:
"""Verify that an event media attachment is successfully resolved."""
await setup_platform()
assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front")
assert camera is not None
device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
assert device
assert device.name == DEVICE_NAME
# Capture any events published
received_events = async_capture_events(hass, NEST_EVENT)
# Set up fake media, and publish image events
auth.responses = [
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
]
event_timestamp = dt_util.now()
await subscriber.async_receive_event(
create_event(
EVENT_SESSION_ID,
EVENT_ID,
PERSON_EVENT,
timestamp=event_timestamp,
)
)
await hass.async_block_till_done()
assert len(received_events) == 1
received_event = received_events[0]
attachment = received_event.data.get("attachment")
assert attachment
assert list(attachment.keys()) == ["image"]
assert attachment["image"].startswith("/api/nest/event_media")
assert attachment["image"].endswith("/thumbnail")
# Download the attachment content and verify it works
client = await hass_client()
response = await client.get(attachment["image"])
assert response.status == HTTPStatus.OK, f"Response not matched: {response}"
await response.read()
@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS])
async def test_event_clip_media_attachment(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
device_registry: dr.DeviceRegistry,
subscriber,
auth,
setup_platform,
mp4,
) -> None:
"""Verify that an event media attachment is successfully resolved."""
await setup_platform()
assert len(hass.states.async_all()) == 1
camera = hass.states.get("camera.front")
assert camera is not None
device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_ID)})
assert device
assert device.name == DEVICE_NAME
# Capture any events published
received_events = async_capture_events(hass, NEST_EVENT)
# Set up fake media, and publish clip events
auth.responses = [
aiohttp.web.Response(body=mp4.getvalue()),
]
event_timestamp = dt_util.now()
await subscriber.async_receive_event(
create_event_message(
create_battery_event_data(MOTION_EVENT),
timestamp=event_timestamp,
)
)
await hass.async_block_till_done()
assert len(received_events) == 1
received_event = received_events[0]
attachment = received_event.data.get("attachment")
assert attachment
assert list(attachment.keys()) == ["image", "video"]
assert attachment["image"].startswith("/api/nest/event_media")
assert attachment["image"].endswith("/thumbnail")
assert attachment["video"].startswith("/api/nest/event_media")
assert not attachment["video"].endswith("/thumbnail")
# Download the attachment content and verify it works
for content_path in attachment.values():
client = await hass_client()
response = await client.get(content_path)
assert response.status == HTTPStatus.OK, f"Response not matched: {response}"
await response.read()