Add UniFi Protect views (#74190)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
0028dc46e6
commit
1555f40bad
4 changed files with 647 additions and 0 deletions
|
@ -40,6 +40,7 @@ from .discovery import async_start_discovery
|
||||||
from .migrate import async_migrate_data
|
from .migrate import async_migrate_data
|
||||||
from .services import async_cleanup_services, async_setup_services
|
from .services import async_cleanup_services, async_setup_services
|
||||||
from .utils import _async_unifi_mac_from_hass, async_get_devices
|
from .utils import _async_unifi_mac_from_hass, async_get_devices
|
||||||
|
from .views import ThumbnailProxyView, VideoProxyView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -92,6 +93,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
async_setup_services(hass)
|
async_setup_services(hass)
|
||||||
|
hass.http.register_view(ThumbnailProxyView(hass))
|
||||||
|
hass.http.register_view(VideoProxyView(hass))
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
|
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
|
|
211
homeassistant/components/unifiprotect/views.py
Normal file
211
homeassistant/components/unifiprotect/views.py
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
"""UniFi Protect Integration views."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from http import HTTPStatus
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from pyunifiprotect.data import Event
|
||||||
|
from pyunifiprotect.exceptions import ClientError
|
||||||
|
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .data import ProtectData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_generate_thumbnail_url(
|
||||||
|
event_id: str,
|
||||||
|
nvr_id: str,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Generate URL for event thumbnail."""
|
||||||
|
|
||||||
|
url_format = ThumbnailProxyView.url or "{nvr_id}/{event_id}"
|
||||||
|
url = url_format.format(nvr_id=nvr_id, event_id=event_id)
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if width is not None:
|
||||||
|
params["width"] = str(width)
|
||||||
|
if height is not None:
|
||||||
|
params["height"] = str(height)
|
||||||
|
|
||||||
|
return f"{url}?{urlencode(params)}"
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_generate_event_video_url(event: Event) -> str:
|
||||||
|
"""Generate URL for event video."""
|
||||||
|
|
||||||
|
_validate_event(event)
|
||||||
|
if event.start is None or event.end is None:
|
||||||
|
raise ValueError("Event is ongoing")
|
||||||
|
|
||||||
|
url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}"
|
||||||
|
url = url_format.format(
|
||||||
|
nvr_id=event.api.bootstrap.nvr.id,
|
||||||
|
camera_id=event.camera_id,
|
||||||
|
start=event.start.isoformat(),
|
||||||
|
end=event.end.isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _client_error(message: Any, code: HTTPStatus) -> web.Response:
|
||||||
|
_LOGGER.warning("Client error (%s): %s", code.value, message)
|
||||||
|
if code == HTTPStatus.BAD_REQUEST:
|
||||||
|
return web.Response(body=message, status=code)
|
||||||
|
return web.Response(status=code)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _400(message: Any) -> web.Response:
|
||||||
|
return _client_error(message, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _403(message: Any) -> web.Response:
|
||||||
|
return _client_error(message, HTTPStatus.FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _404(message: Any) -> web.Response:
|
||||||
|
return _client_error(message, HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _validate_event(event: Event) -> None:
|
||||||
|
if event.camera is None:
|
||||||
|
raise ValueError("Event does not have a camera")
|
||||||
|
if not event.camera.can_read_media(event.api.bootstrap.auth_user):
|
||||||
|
raise PermissionError(f"User cannot read media from camera: {event.camera.id}")
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectProxyView(HomeAssistantView):
|
||||||
|
"""Base class to proxy request to UniFi Protect console."""
|
||||||
|
|
||||||
|
requires_auth = True
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize a thumbnail proxy view."""
|
||||||
|
self.hass = hass
|
||||||
|
self.data = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response:
|
||||||
|
all_data: list[ProtectData] = []
|
||||||
|
|
||||||
|
for data in self.data.values():
|
||||||
|
if isinstance(data, ProtectData):
|
||||||
|
if data.api.bootstrap.nvr.id == nvr_id:
|
||||||
|
return data
|
||||||
|
all_data.append(data)
|
||||||
|
return _404("Invalid NVR ID")
|
||||||
|
|
||||||
|
|
||||||
|
class ThumbnailProxyView(ProtectProxyView):
|
||||||
|
"""View to proxy event thumbnails from UniFi Protect."""
|
||||||
|
|
||||||
|
url = "/api/unifiprotect/thumbnail/{nvr_id}/{event_id}"
|
||||||
|
name = "api:unifiprotect_thumbnail"
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, request: web.Request, nvr_id: str, event_id: str
|
||||||
|
) -> web.Response:
|
||||||
|
"""Get Event Thumbnail."""
|
||||||
|
|
||||||
|
data = self._get_data_or_404(nvr_id)
|
||||||
|
if isinstance(data, web.Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
width: int | str | None = request.query.get("width")
|
||||||
|
height: int | str | None = request.query.get("height")
|
||||||
|
|
||||||
|
if width is not None:
|
||||||
|
try:
|
||||||
|
width = int(width)
|
||||||
|
except ValueError:
|
||||||
|
return _400("Invalid width param")
|
||||||
|
if height is not None:
|
||||||
|
try:
|
||||||
|
height = int(height)
|
||||||
|
except ValueError:
|
||||||
|
return _400("Invalid height param")
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumbnail = await data.api.get_event_thumbnail(
|
||||||
|
event_id, width=width, height=height
|
||||||
|
)
|
||||||
|
except ClientError as err:
|
||||||
|
return _404(err)
|
||||||
|
|
||||||
|
if thumbnail is None:
|
||||||
|
return _404("Event thumbnail not found")
|
||||||
|
|
||||||
|
return web.Response(body=thumbnail, content_type="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
class VideoProxyView(ProtectProxyView):
|
||||||
|
"""View to proxy video clips from UniFi Protect."""
|
||||||
|
|
||||||
|
url = "/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}"
|
||||||
|
name = "api:unifiprotect_thumbnail"
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str
|
||||||
|
) -> web.StreamResponse:
|
||||||
|
"""Get Camera Video clip."""
|
||||||
|
|
||||||
|
data = self._get_data_or_404(nvr_id)
|
||||||
|
if isinstance(data, web.Response):
|
||||||
|
return data
|
||||||
|
|
||||||
|
camera = data.api.bootstrap.cameras.get(camera_id)
|
||||||
|
if camera is None:
|
||||||
|
return _404(f"Invalid camera ID: {camera_id}")
|
||||||
|
if not camera.can_read_media(data.api.bootstrap.auth_user):
|
||||||
|
return _403(f"User cannot read media from camera: {camera.id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_dt = datetime.fromisoformat(start)
|
||||||
|
except ValueError:
|
||||||
|
return _400("Invalid start")
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_dt = datetime.fromisoformat(end)
|
||||||
|
except ValueError:
|
||||||
|
return _400("Invalid end")
|
||||||
|
|
||||||
|
response = web.StreamResponse(
|
||||||
|
status=200,
|
||||||
|
reason="OK",
|
||||||
|
headers={
|
||||||
|
"Content-Type": "video/mp4",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def iterator(total: int, chunk: bytes | None) -> None:
|
||||||
|
if not response.prepared:
|
||||||
|
response.content_length = total
|
||||||
|
await response.prepare(request)
|
||||||
|
|
||||||
|
if chunk is not None:
|
||||||
|
await response.write(chunk)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await camera.get_video(start_dt, end_dt, iterator_callback=iterator)
|
||||||
|
except ClientError as err:
|
||||||
|
return _404(err)
|
||||||
|
|
||||||
|
if response.prepared:
|
||||||
|
await response.write_eof()
|
||||||
|
return response
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from functools import partial
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -102,6 +103,11 @@ def mock_ufp_client(bootstrap: Bootstrap):
|
||||||
"""Mock ProtectApiClient for testing."""
|
"""Mock ProtectApiClient for testing."""
|
||||||
client = Mock()
|
client = Mock()
|
||||||
client.bootstrap = bootstrap
|
client.bootstrap = bootstrap
|
||||||
|
client._bootstrap = bootstrap
|
||||||
|
client.api_path = "/api"
|
||||||
|
# functionality from API client tests actually need
|
||||||
|
client._stream_response = partial(ProtectApiClient._stream_response, client)
|
||||||
|
client.get_camera_video = partial(ProtectApiClient.get_camera_video, client)
|
||||||
|
|
||||||
nvr = client.bootstrap.nvr
|
nvr = client.bootstrap.nvr
|
||||||
nvr._api = client
|
nvr._api = client
|
||||||
|
|
427
tests/components/unifiprotect/test_views.py
Normal file
427
tests/components/unifiprotect/test_views.py
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
"""Test UniFi Protect views."""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, cast
|
||||||
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
|
from aiohttp import ClientResponse
|
||||||
|
import pytest
|
||||||
|
from pyunifiprotect.data import Camera, Event, EventType
|
||||||
|
from pyunifiprotect.exceptions import ClientError
|
||||||
|
|
||||||
|
from homeassistant.components.unifiprotect.views import (
|
||||||
|
async_generate_event_video_url,
|
||||||
|
async_generate_thumbnail_url,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .utils import MockUFPFixture, init_entry
|
||||||
|
|
||||||
|
from tests.test_util.aiohttp import mock_aiohttp_client
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thumbnail_bad_nvr_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
) -> None:
|
||||||
|
"""Test invalid NVR ID in URL."""
|
||||||
|
|
||||||
|
ufp.api.get_event_thumbnail = AsyncMock()
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
url = async_generate_thumbnail_url("test_id", "bad_id")
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.get_event_thumbnail.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("width,height", [("test", None), (None, "test")])
|
||||||
|
async def test_thumbnail_bad_params(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
width: Any,
|
||||||
|
height: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test invalid bad query parameters."""
|
||||||
|
|
||||||
|
ufp.api.get_event_thumbnail = AsyncMock()
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
url = async_generate_thumbnail_url(
|
||||||
|
"test_id", ufp.api.bootstrap.nvr.id, width=width, height=height
|
||||||
|
)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 400
|
||||||
|
ufp.api.get_event_thumbnail.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thumbnail_bad_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
) -> None:
|
||||||
|
"""Test invalid with error raised."""
|
||||||
|
|
||||||
|
ufp.api.get_event_thumbnail = AsyncMock(side_effect=ClientError())
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thumbnail_no_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
) -> None:
|
||||||
|
"""Test invalid no thumbnail returned."""
|
||||||
|
|
||||||
|
ufp.api.get_event_thumbnail = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thumbnail(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
) -> None:
|
||||||
|
"""Test invalid NVR ID in URL."""
|
||||||
|
|
||||||
|
ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest")
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.content_type == "image/jpeg"
|
||||||
|
assert await response.content.read() == b"testtest"
|
||||||
|
ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test generating event with bad camera ID."""
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id="test_id",
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_event_ongoing(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test generating event with bad camera ID."""
|
||||||
|
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=None,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_perms(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test generating event with bad user permissions."""
|
||||||
|
|
||||||
|
ufp.api.bootstrap.auth_user.all_permissions = []
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PermissionError):
|
||||||
|
async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_nvr_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with bad NVR id."""
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock()
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
url = url.replace(ufp.api.bootstrap.nvr.id, "bad_id")
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.request.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_camera_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with bad camera id."""
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock()
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
url = url.replace(camera.id, "bad_id")
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.request.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_camera_perms(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with bad camera perms."""
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock()
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=fixed_now - timedelta(seconds=30),
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
ufp.api.bootstrap.auth_user.all_permissions = []
|
||||||
|
ufp.api.bootstrap.auth_user._perm_cache = {}
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 403
|
||||||
|
ufp.api.request.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("start,end", [("test", None), (None, "test")])
|
||||||
|
async def test_video_bad_params(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
start: Any,
|
||||||
|
end: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with bad start/end params."""
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock()
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event_start = fixed_now - timedelta(seconds=30)
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=event_start,
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
from_value = event_start if start is not None else fixed_now
|
||||||
|
to_value = start if start is not None else end
|
||||||
|
url = url.replace(from_value.isoformat(), to_value)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 400
|
||||||
|
ufp.api.request.assert_not_called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video_bad_video(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with no video."""
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock(side_effect=ClientError)
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event_start = fixed_now - timedelta(seconds=30)
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=event_start,
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
|
||||||
|
assert response.status == 404
|
||||||
|
ufp.api.request.assert_called_once
|
||||||
|
|
||||||
|
|
||||||
|
async def test_video(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: mock_aiohttp_client,
|
||||||
|
ufp: MockUFPFixture,
|
||||||
|
camera: Camera,
|
||||||
|
fixed_now: datetime,
|
||||||
|
) -> None:
|
||||||
|
"""Test video URL with no video."""
|
||||||
|
|
||||||
|
content = Mock()
|
||||||
|
content.__anext__ = AsyncMock(side_effect=[b"test", b"test", StopAsyncIteration()])
|
||||||
|
content.__aiter__ = Mock(return_value=content)
|
||||||
|
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.content_length = 8
|
||||||
|
mock_response.content.iter_chunked = Mock(return_value=content)
|
||||||
|
|
||||||
|
ufp.api.request = AsyncMock(return_value=mock_response)
|
||||||
|
await init_entry(hass, ufp, [camera])
|
||||||
|
|
||||||
|
event_start = fixed_now - timedelta(seconds=30)
|
||||||
|
event = Event(
|
||||||
|
api=ufp.api,
|
||||||
|
camera_id=camera.id,
|
||||||
|
start=event_start,
|
||||||
|
end=fixed_now,
|
||||||
|
id="test_id",
|
||||||
|
type=EventType.MOTION,
|
||||||
|
score=100,
|
||||||
|
smart_detect_types=[],
|
||||||
|
smart_detect_event_ids=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
url = async_generate_event_video_url(event)
|
||||||
|
|
||||||
|
http_client = await hass_client()
|
||||||
|
response = cast(ClientResponse, await http_client.get(url))
|
||||||
|
assert await response.content.read() == b"testtest"
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
ufp.api.request.assert_called_once
|
Loading…
Add table
Add a link
Reference in a new issue