Improve nest media player clip/image and event handling for multiple events in a short time range (#63149)
This commit is contained in:
parent
42706f780c
commit
789c0a24dd
6 changed files with 594 additions and 259 deletions
|
@ -31,6 +31,8 @@ from .common import (
|
|||
create_config_entry,
|
||||
)
|
||||
|
||||
from tests.common import async_capture_events
|
||||
|
||||
DOMAIN = "nest"
|
||||
DEVICE_ID = "example/api/device/id"
|
||||
DEVICE_NAME = "Front"
|
||||
|
@ -70,6 +72,7 @@ GENERATE_IMAGE_URL_RESPONSE = {
|
|||
}
|
||||
IMAGE_BYTES_FROM_EVENT = b"test url image bytes"
|
||||
IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"}
|
||||
NEST_EVENT = "nest_event"
|
||||
|
||||
|
||||
async def async_setup_devices(hass, auth, device_type, traits={}, events=[]):
|
||||
|
@ -85,10 +88,8 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]):
|
|||
),
|
||||
}
|
||||
subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
|
||||
if events:
|
||||
for event in events:
|
||||
await subscriber.async_receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
# Enable feature for fetching media
|
||||
subscriber.cache_policy.fetch = True
|
||||
return subscriber
|
||||
|
||||
|
||||
|
@ -223,20 +224,8 @@ async def test_integration_unloaded(hass, auth):
|
|||
|
||||
async def test_camera_event(hass, auth, hass_client):
|
||||
"""Test a media source and image created for an event."""
|
||||
event_timestamp = dt_util.now()
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
CAMERA_TRAITS,
|
||||
events=[
|
||||
create_event(
|
||||
EVENT_SESSION_ID,
|
||||
EVENT_ID,
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp,
|
||||
),
|
||||
],
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
@ -248,6 +237,31 @@ async def test_camera_event(hass, auth, hass_client):
|
|||
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]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_person"
|
||||
event_identifier = received_event.data["nest_event_id"]
|
||||
|
||||
# Media root directory
|
||||
browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}")
|
||||
assert browse.title == "Nest"
|
||||
|
@ -273,7 +287,7 @@ async def test_camera_event(hass, auth, hass_client):
|
|||
# The device expands recent events
|
||||
assert len(browse.children) == 1
|
||||
assert browse.children[0].domain == DOMAIN
|
||||
assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}"
|
||||
assert browse.children[0].identifier == f"{device.id}/{event_identifier}"
|
||||
event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[0].title == f"Person @ {event_timestamp_string}"
|
||||
assert not browse.children[0].can_expand
|
||||
|
@ -281,10 +295,10 @@ async def test_camera_event(hass, auth, hass_client):
|
|||
|
||||
# Browse to the event
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}"
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}"
|
||||
)
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == f"{device.id}/{EVENT_SESSION_ID}"
|
||||
assert browse.identifier == f"{device.id}/{event_identifier}"
|
||||
assert "Person" in browse.title
|
||||
assert not browse.can_expand
|
||||
assert not browse.children
|
||||
|
@ -292,16 +306,11 @@ async def test_camera_event(hass, auth, hass_client):
|
|||
|
||||
# Resolving the event links to the media
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}"
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}"
|
||||
assert media.mime_type == "image/jpeg"
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
|
@ -311,30 +320,39 @@ async def test_camera_event(hass, auth, hass_client):
|
|||
|
||||
async def test_event_order(hass, auth):
|
||||
"""Test multiple events are in descending timestamp order."""
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
|
||||
)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
event_session_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
|
||||
event_timestamp1 = dt_util.now()
|
||||
await subscriber.async_receive_event(
|
||||
create_event(
|
||||
event_session_id1,
|
||||
EVENT_ID + "1",
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp1,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
event_session_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..."
|
||||
event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5)
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
CAMERA_TRAITS,
|
||||
events=[
|
||||
create_event(
|
||||
event_session_id1,
|
||||
EVENT_ID + "1",
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp1,
|
||||
),
|
||||
create_event(
|
||||
event_session_id2,
|
||||
EVENT_ID + "2",
|
||||
MOTION_EVENT,
|
||||
timestamp=event_timestamp2,
|
||||
),
|
||||
],
|
||||
await subscriber.async_receive_event(
|
||||
create_event(
|
||||
event_session_id2,
|
||||
EVENT_ID + "2",
|
||||
MOTION_EVENT,
|
||||
timestamp=event_timestamp2,
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
|
@ -356,7 +374,6 @@ async def test_event_order(hass, auth):
|
|||
# Motion event is most recent
|
||||
assert len(browse.children) == 2
|
||||
assert browse.children[0].domain == DOMAIN
|
||||
assert browse.children[0].identifier == f"{device.id}/{event_session_id2}"
|
||||
event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
|
||||
assert not browse.children[0].can_expand
|
||||
|
@ -364,14 +381,219 @@ async def test_event_order(hass, auth):
|
|||
|
||||
# Person event is next
|
||||
assert browse.children[1].domain == DOMAIN
|
||||
|
||||
assert browse.children[1].identifier == f"{device.id}/{event_session_id1}"
|
||||
event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[1].title == f"Person @ {event_timestamp_string}"
|
||||
assert not browse.children[1].can_expand
|
||||
assert not browse.children[1].can_play
|
||||
|
||||
|
||||
async def test_multiple_image_events_in_session(hass, auth, hass_client):
|
||||
"""Test multiple events published within the same event session."""
|
||||
event_session_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
|
||||
event_timestamp1 = dt_util.now()
|
||||
event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5)
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
assert camera is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
|
||||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
# Capture any events published
|
||||
received_events = async_capture_events(hass, NEST_EVENT)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT + b"-1"),
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT + b"-2"),
|
||||
]
|
||||
await subscriber.async_receive_event(
|
||||
# First camera sees motion then it recognizes a person
|
||||
create_event(
|
||||
event_session_id,
|
||||
EVENT_ID + "1",
|
||||
MOTION_EVENT,
|
||||
timestamp=event_timestamp1,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await subscriber.async_receive_event(
|
||||
create_event(
|
||||
event_session_id,
|
||||
EVENT_ID + "2",
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp2,
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(received_events) == 2
|
||||
received_event = received_events[0]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_motion"
|
||||
event_identifier1 = received_event.data["nest_event_id"]
|
||||
received_event = received_events[1]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_person"
|
||||
event_identifier2 = received_event.data["nest_event_id"]
|
||||
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}"
|
||||
)
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == device.id
|
||||
assert browse.title == "Front: Recent Events"
|
||||
assert browse.can_expand
|
||||
|
||||
# Person event is most recent
|
||||
assert len(browse.children) == 2
|
||||
event = browse.children[0]
|
||||
assert event.domain == DOMAIN
|
||||
assert event.identifier == f"{device.id}/{event_identifier2}"
|
||||
event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT)
|
||||
assert event.title == f"Person @ {event_timestamp_string}"
|
||||
assert not event.can_expand
|
||||
assert not event.can_play
|
||||
|
||||
# Motion event is next
|
||||
event = browse.children[1]
|
||||
assert event.domain == DOMAIN
|
||||
assert event.identifier == f"{device.id}/{event_identifier1}"
|
||||
event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT)
|
||||
assert event.title == f"Motion @ {event_timestamp_string}"
|
||||
assert not event.can_expand
|
||||
assert not event.can_play
|
||||
|
||||
# Resolve the most recent event
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier2}"
|
||||
assert media.mime_type == "image/jpeg"
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT + b"-2"
|
||||
|
||||
# Resolving the event links to the media
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}"
|
||||
assert media.mime_type == "image/jpeg"
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT + b"-1"
|
||||
|
||||
|
||||
async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client):
|
||||
"""Test multiple events published within the same event session."""
|
||||
event_timestamp1 = dt_util.now()
|
||||
event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5)
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
assert camera is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
|
||||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
# Capture any events published
|
||||
received_events = async_capture_events(hass, NEST_EVENT)
|
||||
|
||||
# Publish two events: First motion, then a person is recognized. Both
|
||||
# events share a single clip.
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
await subscriber.async_receive_event(
|
||||
create_event_message(
|
||||
create_battery_event_data(MOTION_EVENT),
|
||||
timestamp=event_timestamp1,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await subscriber.async_receive_event(
|
||||
create_event_message(
|
||||
create_battery_event_data(PERSON_EVENT),
|
||||
timestamp=event_timestamp2,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(received_events) == 2
|
||||
received_event = received_events[0]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_motion"
|
||||
event_identifier1 = received_event.data["nest_event_id"]
|
||||
received_event = received_events[1]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_person"
|
||||
event_identifier2 = received_event.data["nest_event_id"]
|
||||
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}"
|
||||
)
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == device.id
|
||||
assert browse.title == "Front: Recent Events"
|
||||
assert browse.can_expand
|
||||
|
||||
# The two distinct events are combined in a single clip preview
|
||||
assert len(browse.children) == 1
|
||||
event = browse.children[0]
|
||||
assert event.domain == DOMAIN
|
||||
event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT)
|
||||
assert event.identifier == f"{device.id}/{event_identifier2}"
|
||||
assert event.title == f"Motion, Person @ {event_timestamp_string}"
|
||||
assert not event.can_expand
|
||||
assert event.can_play
|
||||
|
||||
# Resolve media for each event that was published and they will resolve
|
||||
# to the same clip preview media clip object.
|
||||
# Resolve media for the first event
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
# Resolve media for the second event
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
|
||||
async def test_browse_invalid_device_id(hass, auth):
|
||||
"""Test a media source request for an invalid device id."""
|
||||
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS)
|
||||
|
@ -451,29 +673,39 @@ async def test_resolve_invalid_event_id(hass, auth):
|
|||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
with pytest.raises(Unresolvable):
|
||||
await media_source.async_resolve_media(
|
||||
hass,
|
||||
f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...",
|
||||
)
|
||||
# Assume any event ID can be resolved to a media url. Fetching the actual media may fail
|
||||
# if the ID is not valid. Content type is inferred based on the capabilities of the device.
|
||||
media = await media_source.async_resolve_media(
|
||||
hass,
|
||||
f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...",
|
||||
)
|
||||
assert (
|
||||
media.url == f"/api/nest/event_media/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW..."
|
||||
)
|
||||
assert media.mime_type == "image/jpeg"
|
||||
|
||||
|
||||
async def test_camera_event_clip_preview(hass, auth, hass_client):
|
||||
"""Test an event for a battery camera video clip."""
|
||||
event_timestamp = dt_util.now()
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
BATTERY_CAMERA_TRAITS,
|
||||
events=[
|
||||
create_event_message(
|
||||
create_battery_event_data(MOTION_EVENT),
|
||||
timestamp=event_timestamp,
|
||||
),
|
||||
],
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
|
||||
)
|
||||
|
||||
# Capture any events published
|
||||
received_events = async_capture_events(hass, NEST_EVENT)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
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(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
assert camera is not None
|
||||
|
@ -483,6 +715,13 @@ async def test_camera_event_clip_preview(hass, auth, hass_client):
|
|||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
# Verify events are published correctly
|
||||
assert len(received_events) == 1
|
||||
received_event = received_events[0]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_motion"
|
||||
event_identifier = received_event.data["nest_event_id"]
|
||||
|
||||
# Browse to the device
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}"
|
||||
|
@ -494,24 +733,34 @@ async def test_camera_event_clip_preview(hass, auth, hass_client):
|
|||
# The device expands recent events
|
||||
assert len(browse.children) == 1
|
||||
assert browse.children[0].domain == DOMAIN
|
||||
actual_event_id = browse.children[0].identifier
|
||||
assert browse.children[0].identifier == f"{device.id}/{event_identifier}"
|
||||
event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
|
||||
assert not browse.children[0].can_expand
|
||||
assert len(browse.children[0].children) == 0
|
||||
assert browse.children[0].can_play
|
||||
|
||||
# Verify received event and media ids match
|
||||
assert browse.children[0].identifier == f"{device.id}/{event_identifier}"
|
||||
|
||||
# Browse to the event
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}"
|
||||
)
|
||||
assert browse.domain == DOMAIN
|
||||
event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT)
|
||||
assert browse.title == f"Motion @ {event_timestamp_string}"
|
||||
assert not browse.can_expand
|
||||
assert len(browse.children) == 0
|
||||
assert browse.can_play
|
||||
|
||||
# Resolving the event links to the media
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{actual_event_id}"
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{actual_event_id}"
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
|
@ -548,22 +797,24 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client):
|
|||
|
||||
async def test_event_media_failure(hass, auth, hass_client):
|
||||
"""Test event media fetch sees a failure from the server."""
|
||||
event_timestamp = dt_util.now()
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
CAMERA_TRAITS,
|
||||
events=[
|
||||
create_event(
|
||||
EVENT_SESSION_ID,
|
||||
EVENT_ID,
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp,
|
||||
),
|
||||
],
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS
|
||||
)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR),
|
||||
]
|
||||
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(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
assert camera is not None
|
||||
|
@ -580,10 +831,6 @@ async def test_event_media_failure(hass, auth, hass_client):
|
|||
assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
assert media.mime_type == "image/jpeg"
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR),
|
||||
]
|
||||
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR, (
|
||||
|
@ -593,21 +840,7 @@ async def test_event_media_failure(hass, auth, hass_client):
|
|||
|
||||
async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user):
|
||||
"""Test case where user does not have permissions to view media."""
|
||||
event_timestamp = dt_util.now()
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
CAMERA_TRAITS,
|
||||
events=[
|
||||
create_event(
|
||||
EVENT_SESSION_ID,
|
||||
EVENT_ID,
|
||||
PERSON_EVENT,
|
||||
timestamp=event_timestamp,
|
||||
),
|
||||
],
|
||||
)
|
||||
await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
|
@ -618,7 +851,7 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin
|
|||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
media_url = f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
media_url = f"/api/nest/event_media/{device.id}/some-event-id"
|
||||
|
||||
# Empty policy with no access to the entity
|
||||
hass_admin_user.mock_policy({})
|
||||
|
@ -673,6 +906,10 @@ async def test_multiple_devices(hass, auth, hass_client):
|
|||
|
||||
# Send events for device #1
|
||||
for i in range(0, 5):
|
||||
auth.responses = [
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
await subscriber.async_receive_event(
|
||||
create_event(
|
||||
f"event-session-id-{i}",
|
||||
|
@ -681,6 +918,7 @@ async def test_multiple_devices(hass, auth, hass_client):
|
|||
device_id=device_id1,
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}"
|
||||
|
@ -693,11 +931,16 @@ async def test_multiple_devices(hass, auth, hass_client):
|
|||
|
||||
# Send events for device #2
|
||||
for i in range(0, 3):
|
||||
auth.responses = [
|
||||
aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
await subscriber.async_receive_event(
|
||||
create_event(
|
||||
f"other-id-{i}", f"event-id{i}", PERSON_EVENT, device_id=device_id2
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}"
|
||||
|
@ -761,6 +1004,7 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
|
|||
create_battery_event_data(MOTION_EVENT), timestamp=event_timestamp
|
||||
)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Browse to event
|
||||
browse = await media_source.async_browse_media(
|
||||
|
@ -768,16 +1012,16 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
|
|||
)
|
||||
assert len(browse.children) == 1
|
||||
assert browse.children[0].domain == DOMAIN
|
||||
assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}"
|
||||
event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
|
||||
assert not browse.children[0].can_expand
|
||||
assert browse.children[0].can_play
|
||||
event_identifier = browse.children[0].identifier
|
||||
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}"
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
assert media.url == f"/api/nest/event_media/{event_identifier}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
# Fetch event media
|
||||
|
@ -801,8 +1045,6 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
|
|||
subscriber = FakeSubscriber()
|
||||
device_manager = await subscriber.async_get_device_manager()
|
||||
device_manager.add_device(nest_device)
|
||||
# Fetch media for events when published
|
||||
subscriber.cache_policy.fetch = True
|
||||
|
||||
with patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation"
|
||||
|
@ -824,16 +1066,16 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
|
|||
)
|
||||
assert len(browse.children) == 1
|
||||
assert browse.children[0].domain == DOMAIN
|
||||
assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}"
|
||||
event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT)
|
||||
assert browse.children[0].title == f"Motion @ {event_timestamp_string}"
|
||||
assert not browse.children[0].can_expand
|
||||
assert browse.children[0].can_play
|
||||
event_identifier = browse.children[0].identifier
|
||||
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}"
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
assert media.url == f"/api/nest/event_media/{event_identifier}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
# Verify media exists
|
||||
|
@ -843,20 +1085,63 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store):
|
|||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
|
||||
async def test_media_store_filesystem_error(hass, auth, hass_client):
|
||||
"""Test a filesystem error read/writing event media."""
|
||||
async def test_media_store_save_filesystem_error(hass, auth, hass_client):
|
||||
"""Test a filesystem error writing event media."""
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
|
||||
)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
event_timestamp = dt_util.now()
|
||||
await async_setup_devices(
|
||||
hass,
|
||||
auth,
|
||||
CAMERA_DEVICE_TYPE,
|
||||
BATTERY_CAMERA_TRAITS,
|
||||
events=[
|
||||
# The client fetches the media from the server, but has a failure when
|
||||
# persisting the media to disk.
|
||||
client = await hass_client()
|
||||
with patch("homeassistant.components.nest.media_source.open", side_effect=OSError):
|
||||
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(hass.states.async_all()) == 1
|
||||
camera = hass.states.get("camera.front")
|
||||
assert camera is not None
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
|
||||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
browse = await media_source.async_browse_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}"
|
||||
)
|
||||
assert browse.domain == DOMAIN
|
||||
assert browse.identifier == device.id
|
||||
assert len(browse.children) == 1
|
||||
event = browse.children[0]
|
||||
|
||||
media = await media_source.async_resolve_media(
|
||||
hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}"
|
||||
)
|
||||
assert media.url == f"/api/nest/event_media/{event.identifier}"
|
||||
assert media.mime_type == "video/mp4"
|
||||
|
||||
# We fail to retrieve the media from the server since the origin filesystem op failed
|
||||
client = await hass_client()
|
||||
response = await client.get(media.url)
|
||||
assert response.status == HTTPStatus.NOT_FOUND, (
|
||||
"Response not matched: %s" % response
|
||||
)
|
||||
|
||||
|
||||
async def test_media_store_load_filesystem_error(hass, auth, hass_client):
|
||||
"""Test a filesystem error reading event media."""
|
||||
subscriber = await async_setup_devices(
|
||||
hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS
|
||||
)
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
@ -868,64 +1153,38 @@ async def test_media_store_filesystem_error(hass, auth, hass_client):
|
|||
assert device
|
||||
assert device.name == DEVICE_NAME
|
||||
|
||||
# Capture any events published
|
||||
received_events = async_capture_events(hass, NEST_EVENT)
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
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()
|
||||
|
||||
# The client fetches the media from the server, but has a failure when
|
||||
# persisting the media to disk.
|
||||
assert len(received_events) == 1
|
||||
received_event = received_events[0]
|
||||
assert received_event.data["device_id"] == device.id
|
||||
assert received_event.data["type"] == "camera_motion"
|
||||
event_identifier = received_event.data["nest_event_id"]
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
# Fetch the media from the server, and simluate a failure reading from disk
|
||||
client = await hass_client()
|
||||
with patch("homeassistant.components.nest.media_source.open", side_effect=OSError):
|
||||
response = await client.get(
|
||||
f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
f"/api/nest/event_media/{device.id}/{event_identifier}"
|
||||
)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Fetch the media again, and since the object does not exist in the cache it
|
||||
# needs to be fetched again. The server returns an error to prove that it was
|
||||
# not a cache read. A second attempt succeeds.
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR),
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
# First attempt, server fails when fetching
|
||||
response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}")
|
||||
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR, (
|
||||
"Response not matched: %s" % response
|
||||
)
|
||||
|
||||
# Second attempt, server responds success
|
||||
response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}")
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
# Third attempt reads from the disk cache with no server fetch
|
||||
response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}")
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
auth.responses = [
|
||||
aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT),
|
||||
]
|
||||
# Exercise a failure reading from the disk cache. Re-populate from server and write to disk ok
|
||||
with patch("homeassistant.components.nest.media_source.open", side_effect=OSError):
|
||||
response = await client.get(
|
||||
f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}"
|
||||
assert response.status == HTTPStatus.NOT_FOUND, (
|
||||
"Response not matched: %s" % response
|
||||
)
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await client.get(f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}")
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == IMAGE_BYTES_FROM_EVENT
|
||||
|
||||
|
||||
async def test_camera_event_media_eviction(hass, auth, hass_client):
|
||||
|
@ -940,9 +1199,6 @@ async def test_camera_event_media_eviction(hass, auth, hass_client):
|
|||
BATTERY_CAMERA_TRAITS,
|
||||
)
|
||||
|
||||
# Media fetched as soon as it is published
|
||||
subscriber.cache_policy.fetch = True
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)})
|
||||
assert device
|
||||
|
@ -1003,13 +1259,13 @@ async def test_camera_event_media_eviction(hass, auth, hass_client):
|
|||
hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}"
|
||||
)
|
||||
assert len(browse.children) == 5
|
||||
child_events = iter(browse.children)
|
||||
|
||||
# Verify all other content is still persisted correctly
|
||||
client = await hass_client()
|
||||
for i in range(3, 8):
|
||||
response = await client.get(
|
||||
f"/api/nest/event_media/{device.id}/event-session-{i}"
|
||||
)
|
||||
for i in reversed(range(3, 8)):
|
||||
child_event = next(child_events)
|
||||
response = await client.get(f"/api/nest/event_media/{child_event.identifier}")
|
||||
assert response.status == HTTPStatus.OK, "Response not matched: %s" % response
|
||||
contents = await response.read()
|
||||
assert contents == f"image-bytes-{i}".encode()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue