Update nest events to include attachment image and video urls (#124554)
parent
3304e27fa3
commit
18212052a4
|
@ -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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue