1 """UniFi Protect Integration views."""
3 from __future__
import annotations
5 from datetime
import datetime
6 from http
import HTTPStatus
8 from typing
import TYPE_CHECKING, Any
9 from urllib.parse
import urlencode
11 from aiohttp
import web
12 from uiprotect.data
import Camera, Event
13 from uiprotect.exceptions
import ClientError
19 from .data
import ProtectData, async_get_data_for_entry_id, async_get_data_for_nvr_id
21 _LOGGER = logging.getLogger(__name__)
28 width: int |
None =
None,
29 height: int |
None =
None,
31 """Generate URL for event thumbnail."""
33 url_format = ThumbnailProxyView.url
35 assert url_format
is not None
36 url = url_format.format(nvr_id=nvr_id, event_id=event_id)
40 params[
"width"] =
str(width)
41 if height
is not None:
42 params[
"height"] =
str(height)
44 return f
"{url}?{urlencode(params)}"
49 """Generate URL for event video."""
52 if event.start
is None or event.end
is None:
53 raise ValueError(
"Event is ongoing")
55 url_format = VideoProxyView.url
57 assert url_format
is not None
58 return url_format.format(
59 nvr_id=event.api.bootstrap.nvr.id,
60 camera_id=event.camera_id,
61 start=event.start.replace(microsecond=0).isoformat(),
62 end=event.end.replace(microsecond=0).isoformat(),
71 """Generate proxy URL for event video."""
73 url_format = VideoEventProxyView.url
75 assert url_format
is not None
76 return url_format.format(nvr_id=nvr_id, event_id=event_id)
81 _LOGGER.warning(
"Client error (%s): %s", code.value, message)
82 if code == HTTPStatus.BAD_REQUEST:
83 return web.Response(body=message, status=code)
84 return web.Response(status=code)
88 def _400(message: Any) -> web.Response:
93 def _403(message: Any) -> web.Response:
98 def _404(message: Any) -> web.Response:
104 if event.camera
is None:
105 raise ValueError(
"Event does not have a camera")
106 if not event.camera.can_read_media(event.api.bootstrap.auth_user):
107 raise PermissionError(f
"User cannot read media from camera: {event.camera.id}")
111 """Base class to proxy request to UniFi Protect console."""
116 """Initialize a thumbnail proxy view."""
125 return _404(
"Invalid NVR ID")
129 if (camera := data.api.bootstrap.cameras.get(camera_id))
is not None:
132 entity_registry = er.async_get(self.
hasshass)
133 device_registry = dr.async_get(self.
hasshass)
135 if (entity := entity_registry.async_get(camera_id))
is None or (
136 device := device_registry.async_get(entity.device_id
or "")
140 macs = [c[1]
for c
in device.connections
if c[0] == dr.CONNECTION_NETWORK_MAC]
142 if (ufp_device := data.api.bootstrap.get_device_from_mac(mac))
is not None:
143 if isinstance(ufp_device, Camera):
150 """View to proxy event thumbnails from UniFi Protect."""
152 url =
"/api/unifiprotect/thumbnail/{nvr_id}/{event_id}"
153 name =
"api:unifiprotect_thumbnail"
156 self, request: web.Request, nvr_id: str, event_id: str
158 """Get Event Thumbnail."""
161 if isinstance(data, web.Response):
164 width: int | str |
None = request.query.get(
"width")
165 height: int | str |
None = request.query.get(
"height")
167 if width
is not None:
171 return _400(
"Invalid width param")
172 if height
is not None:
176 return _400(
"Invalid height param")
179 thumbnail = await data.api.get_event_thumbnail(
180 event_id, width=width, height=height
182 except ClientError
as err:
185 if thumbnail
is None:
186 return _404(
"Event thumbnail not found")
188 return web.Response(body=thumbnail, content_type=
"image/jpeg")
192 """View to proxy video clips from UniFi Protect."""
194 url =
"/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}"
195 name =
"api:unifiprotect_thumbnail"
198 self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str
199 ) -> web.StreamResponse:
200 """Get Camera Video clip."""
203 if isinstance(data, web.Response):
208 return _404(f
"Invalid camera ID: {camera_id}")
209 if not camera.can_read_media(data.api.bootstrap.auth_user):
210 return _403(f
"User cannot read media from camera: {camera.id}")
213 start_dt = datetime.fromisoformat(start)
215 return _400(
"Invalid start")
218 end_dt = datetime.fromisoformat(end)
220 return _400(
"Invalid end")
222 response = web.StreamResponse(
226 "Content-Type":
"video/mp4",
230 async
def iterator(total: int, chunk: bytes |
None) ->
None:
231 if not response.prepared:
232 response.content_length = total
233 await response.prepare(request)
235 if chunk
is not None:
236 await response.write(chunk)
239 await camera.get_video(start_dt, end_dt, iterator_callback=iterator)
240 except ClientError
as err:
243 if response.prepared:
244 await response.write_eof()
249 """View to proxy video clips for events from UniFi Protect."""
251 url =
"/api/unifiprotect/video/{nvr_id}/{event_id}"
252 name =
"api:unifiprotect_videoEventView"
255 self, request: web.Request, nvr_id: str, event_id: str
256 ) -> web.StreamResponse:
257 """Get Camera Video clip for an event."""
260 if isinstance(data, web.Response):
264 event = await data.api.get_event(event_id)
266 return _404(f
"Invalid event ID: {event_id}")
267 if event.start
is None or event.end
is None:
268 return _400(
"Event is still ongoing")
271 return _404(f
"Invalid camera ID: {event.camera_id}")
272 if not camera.can_read_media(data.api.bootstrap.auth_user):
273 return _403(f
"User cannot read media from camera: {camera.id}")
275 response = web.StreamResponse(
279 "Content-Type":
"video/mp4",
283 async
def iterator(total: int, chunk: bytes |
None) ->
None:
284 if not response.prepared:
285 response.content_length = total
286 await response.prepare(request)
288 if chunk
is not None:
289 await response.write(chunk)
292 await camera.get_video(event.start, event.end, iterator_callback=iterator)
293 except ClientError
as err:
296 if response.prepared:
297 await response.write_eof()
Camera|None _async_get_camera(self, ProtectData data, str camera_id)
None __init__(self, HomeAssistant hass)
ProtectData|web.Response _get_data_or_404(self, str nvr_id_or_entry_id)
web.Response get(self, web.Request request, str nvr_id, str event_id)
web.StreamResponse get(self, web.Request request, str nvr_id, str event_id)
web.StreamResponse get(self, web.Request request, str nvr_id, str camera_id, str start, str end)
ProtectData|None async_get_data_for_entry_id(HomeAssistant hass, str entry_id)
ProtectData|None async_get_data_for_nvr_id(HomeAssistant hass, str nvr_id)
str async_generate_event_video_url(Event event)
str async_generate_thumbnail_url(str event_id, str nvr_id, int|None width=None, int|None height=None)
None _validate_event(Event event)
web.Response _400(Any message)
str async_generate_proxy_event_video_url(str nvr_id, str event_id)
web.Response _404(Any message)
web.Response _client_error(Any message, HTTPStatus code)
web.Response _403(Any message)