async def get_camera_video( self, camera_id: str, start: datetime, end: datetime, channel_index: int = 0, validate_channel_id: bool = True) -> Optional[bytes]: """Exports MP4 video from a given camera at a specific time""" if validate_channel_id and self._bootstrap is not None: camera = self._bootstrap.cameras[camera_id] try: camera.channels[channel_index] except IndexError as e: raise BadRequest from e params = { "camera": camera_id, "channel": channel_index, "start": to_js_time(start), "end": to_js_time(end), } return await self.api_request_raw("video/export", params=params, raise_exception=False)
async def test_ws_event_smart(protect_client_no_debug: ProtectApiClient, now, camera, packet: WSPacket): protect_client = protect_client_no_debug def get_camera(): return protect_client.bootstrap.cameras[camera["id"]] bootstrap_before = protect_client.bootstrap.unifi_dict() camera_before = get_camera().copy() expected_updated_id = "0441ecc6-f0fa-4b19-b071-7987c143138a" expected_event_id = "bf9a241afe74821ceffffd05" action_frame: WSJSONPacketFrame = packet.action_frame # type: ignore action_frame.data["newUpdateId"] = expected_updated_id data_frame: WSJSONPacketFrame = packet.data_frame # type: ignore data_frame.data = { "id": expected_event_id, "type": "smartDetectZone", "start": to_js_time(now - timedelta(seconds=30)), "end": to_js_time(now), "score": 0, "smartDetectTypes": ["person"], "smartDetectEvents": [], "camera": camera["id"], "partition": None, "user": None, "metadata": {}, "thumbnail": f"e-{expected_event_id}", "heatmap": f"e-{expected_event_id}", "modelKey": "event", } msg = MagicMock() msg.data = packet.pack_frames() protect_client._process_ws_message(msg) bootstrap_before["lastUpdateId"] = expected_updated_id bootstrap = protect_client.bootstrap.unifi_dict() camera = get_camera() smart_event = camera.last_smart_detect_event camera.last_smart_detect_event_id = None camera_before.last_smart_detect_event_id = None assert bootstrap == bootstrap_before assert camera.dict() == camera_before.dict() assert smart_event.id == expected_event_id assert smart_event.type == EventType.SMART_DETECT assert smart_event.thumbnail_id == f"e-{expected_event_id}" assert smart_event.heatmap_id == f"e-{expected_event_id}" assert smart_event.start == (now - timedelta(seconds=30)) assert smart_event.end == now for channel in camera.channels: assert channel._api is not None
async def test_ws_emit_alarm_callback( mock_now, protect_client_no_debug: ProtectApiClient, now: datetime, sensor, packet: WSPacket ): mock_now.return_value = now protect_client = protect_client_no_debug protect_client.emit_message = Mock() # type: ignore obj = protect_client.bootstrap.sensors[sensor["id"]] expected_updated_id = "0441ecc6-f0fa-4b19-b071-7987c143138a" action_frame: WSJSONPacketFrame = packet.action_frame # type: ignore action_frame.data = { "action": "update", "newUpdateId": expected_updated_id, "modelKey": "sensor", "id": sensor["id"], } data_frame: WSJSONPacketFrame = packet.data_frame # type: ignore data_frame.data = {"alarmTriggeredAt": to_js_time(now)} msg = MagicMock() msg.data = packet.pack_frames() assert not obj.is_alarm_detected with patch("pyunifiprotect.data.bootstrap.utc_now", mock_now): protect_client._process_ws_message(msg) assert obj.is_alarm_detected mock_now.return_value = utc_now() + EVENT_PING_INTERVAL assert not obj.is_alarm_detected protect_client.emit_message.assert_called_once()
async def test_camera_set_lcd_text_custom_to_custom( camera_obj: Optional[Camera]): camera_obj.api.api_request.reset_mock() camera_obj.feature_flags.has_lcd_screen = True camera_obj.lcd_message = LCDMessage( type=DoorbellMessageType.CUSTOM_MESSAGE, text="Welcome", reset_at=None, ) camera_obj._initial_data = camera_obj.dict() now = datetime.utcnow() await camera_obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, "Test", now) camera_obj.api.api_request.assert_called_with( f"cameras/{camera_obj.id}", method="patch", json={ "lcdMessage": { "type": DoorbellMessageType.CUSTOM_MESSAGE.value, "text": "Test", "resetAt": to_js_time(now), } }, )
async def get_events_raw( self, start: Optional[datetime] = None, end: Optional[datetime] = None, limit: Optional[int] = None, camera_ids: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Get list of events from Protect Args: * `start`: start time for events * `end`: end time for events * `limit`: max number of events to return * `camera_ids`: list of Cameras to get events for If `limit`, `start` and `end` are not provided, it will default to all events in the last 24 hours. If `start` is provided, then `end` or `limit` must be provided. If `end` is provided, then `start` or `limit` must be provided. Otherwise, you will get a 400 error from Unifi Protect Providing a list of Camera IDs will not prevent non-camera events from returning. """ # if no parameters are passed in, default to all events from last 24 hours if limit is None and start is None and end is None: end = utc_now() + timedelta(seconds=10) start = end - timedelta(hours=24) params: Dict[str, Any] = {} if limit is not None: params["limit"] = limit if start is not None: params["start"] = to_js_time(start) if end is not None: params["end"] = to_js_time(end) if camera_ids is not None: params["cameras"] = ",".join(camera_ids) return await self.api_request_list("events", params=params)
async def test_get_events_raw_default(protect_client: ProtectApiClient, now: datetime): events = await protect_client.get_events_raw() end = now + timedelta(seconds=10) protect_client.api_request.assert_called_with( # type: ignore url="events", method="get", require_auth=True, raise_exception=True, params={ "start": to_js_time(end - timedelta(hours=24)), "end": to_js_time(end), }, ) assert len(events) == CONSTANTS["event_count"] for event in events: assert event["type"] in EventType.values() assert event["modelKey"] in ModelType.values()
async def get_events(*args, **kwargs): return [ { "id": expected_event_id, "type": "ring", "start": to_js_time(now - timedelta(seconds=1)), "end": to_js_time(now), "score": 0, "smartDetectTypes": [], "smartDetectEvents": [], "camera": camera["id"], "partition": None, "user": None, "metadata": {}, "thumbnail": f"e-{expected_event_id}", "heatmap": f"e-{expected_event_id}", "modelKey": "event", }, ]
async def test_get_raw_events(old_protect_client: UpvServer, now: datetime): events = await old_protect_client.get_raw_events() old_protect_client.api_request.assert_called_with( # type: ignore url="events", method="get", require_auth=True, raise_exception=True, params={ "end": str(to_js_time(now + timedelta(seconds=10))), "start": str(to_js_time(now - timedelta(seconds=86400))), }, ) assert len(events) == CONSTANTS["event_count"] for event in events: event_type: str = event["type"] model_type: str = event["modelKey"] assert event_type in EventType.values() assert model_type in ModelType.values()
def unifi_dict(self, data: Optional[Dict[str, Any]] = None, exclude: Optional[Set[str]] = None) -> Dict[str, Any]: data = super().unifi_dict(data=data, exclude=exclude) if "text" in data: data["text"] = self._fix_text(data["text"], data.get("type", self.type.value)) if "resetAt" in data: data["resetAt"] = to_js_time(data["resetAt"]) return data
async def test_get_camera_video(protect_client: ProtectApiClient, now, tmp_binary_file): camera = list(protect_client.bootstrap.cameras.values())[0] start = now - timedelta(seconds=CONSTANTS["camera_video_length"]) data = await protect_client.get_camera_video(camera.id, start, now) assert data is not None protect_client.api_request_raw.assert_called_with( # type: ignore "video/export", params={ "camera": camera.id, "channel": 0, "start": to_js_time(start), "end": to_js_time(now), }, raise_exception=False, ) tmp_binary_file.write(data) tmp_binary_file.close() validate_video_file(tmp_binary_file.name, CONSTANTS["camera_video_length"])
async def test_ws_event_update(protect_client_no_debug: ProtectApiClient, now, camera, packet: WSPacket): protect_client = protect_client_no_debug def get_camera() -> Camera: return protect_client.bootstrap.cameras[camera["id"]] bootstrap_before = protect_client.bootstrap.unifi_dict() camera_before = get_camera().copy() new_stats = camera_before.stats.unifi_dict() new_stats["rxBytes"] += 100 new_stats["txBytes"] += 100 new_stats["video"]["recordingEnd"] = to_js_time(now) new_stats_unifi = camera_before.unifi_dict( data={"stats": deepcopy(new_stats)}) del new_stats_unifi["stats"]["wifiQuality"] del new_stats_unifi["stats"]["wifiStrength"] expected_updated_id = "0441ecc6-f0fa-4b19-b071-7987c143138a" action_frame: WSJSONPacketFrame = packet.action_frame # type: ignore action_frame.data = { "action": "update", "newUpdateId": expected_updated_id, "modelKey": "camera", "id": camera["id"], } data_frame: WSJSONPacketFrame = packet.data_frame # type: ignore data_frame.data = new_stats_unifi msg = MagicMock() msg.data = packet.pack_frames() protect_client._process_ws_message(msg) camera_index = -1 for index, camera_dict in enumerate(bootstrap_before["cameras"]): if camera_dict["id"] == camera["id"]: camera_index = index break bootstrap_before["cameras"][camera_index]["stats"] = new_stats bootstrap_before["lastUpdateId"] = expected_updated_id bootstrap = protect_client.bootstrap.unifi_dict() assert bootstrap == bootstrap_before
async def test_get_pacakge_camera_snapshot(protect_client: ProtectApiClient, now): data = await protect_client.get_package_camera_snapshot("test_id") assert data is not None protect_client.api_request_raw.assert_called_with( # type: ignore "cameras/test_id/package-snapshot", params={ "ts": to_js_time(now), "force": "true", }, raise_exception=False, ) img = Image.open(BytesIO(data)) assert img.format in ("PNG", "JPEG")
async def test_camera_set_lcd_text_none(mock_now, camera_obj: Optional[Camera], now: datetime): mock_now.return_value = now if camera_obj is None: pytest.skip("No camera_obj obj found") camera_obj.api.emit_message = Mock() camera_obj.api.api_request.reset_mock() camera_obj.feature_flags.has_lcd_screen = True camera_obj.lcd_message = LCDMessage( type=DoorbellMessageType.DO_NOT_DISTURB, text=DoorbellMessageType.DO_NOT_DISTURB.value.replace("_", " "), reset_at=None, ) camera_obj._initial_data = camera_obj.dict() await camera_obj.set_lcd_text(None) expected_dt = now - timedelta(seconds=10) camera_obj.api.api_request.assert_called_with( f"cameras/{camera_obj.id}", method="patch", json={"lcdMessage": { "resetAt": to_js_time(expected_dt), }}, ) # old/new is actually the same here since the client # generating the message is the one that changed it camera_obj.api.emit_message.assert_called_with( WSSubscriptionMessage( action=WSAction.UPDATE, new_update_id=camera_obj.api.bootstrap.last_update_id, changed_data={"lcd_message": { "reset_at": expected_dt }}, old_obj=camera_obj, new_obj=camera_obj, ))
async def test_get_snapshot(old_protect_client: UpvServer, now, camera): data = await old_protect_client.get_snapshot_image(camera_id=camera["id"]) if data is None: return height = old_protect_client.devices[camera["id"]].get("image_height") width = old_protect_client.devices[camera["id"]].get("image_width") old_protect_client.api_request_raw.assert_called_with( # type: ignore f"cameras/{camera['id']}/snapshot", params={ "h": height, "ts": str(to_js_time(now)), "force": "true", "w": width, }, raise_exception=False, ) img = Image.open(BytesIO(data)) assert img.width == width assert img.height == height
async def get_camera_snapshot( self, camera_id: str, width: Optional[int] = None, height: Optional[int] = None, ) -> Optional[bytes]: """Gets a snapshot from a camera""" dt = utc_now() # ts is only used as a cache buster params = { "ts": to_js_time(dt), "force": "true", } if width is not None: params.update({"w": width}) if height is not None: params.update({"h": height}) return await self.api_request_raw(f"cameras/{camera_id}/snapshot", params=params, raise_exception=False)
async def test_camera_set_lcd_text_default(mock_now, camera_obj: Optional[Camera], now: datetime): mock_now.return_value = now if camera_obj is None: pytest.skip("No camera_obj obj found") camera_obj.api.api_request.reset_mock() camera_obj.feature_flags.has_lcd_screen = True camera_obj.lcd_message = LCDMessage( type=DoorbellMessageType.DO_NOT_DISTURB, text=DoorbellMessageType.DO_NOT_DISTURB.value.replace("_", " "), reset_at=None, ) camera_obj._initial_data = camera_obj.dict() await camera_obj.set_lcd_text(DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR, reset_at=DEFAULT) expected_dt = now + camera_obj.api.bootstrap.nvr.doorbell_settings.default_message_reset_timeout camera_obj.api.api_request.assert_called_with( f"cameras/{camera_obj.id}", method="patch", json={ "lcdMessage": { "type": DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR.value, "text": DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR.value.replace( "_", " "), "resetAt": to_js_time(expected_dt), } }, )