Skip to content

Commit

Permalink
Merge pull request #24 from J3n50m4t/main
Browse files Browse the repository at this point in the history
Add `detection_types` param
  • Loading branch information
ep1cman authored Mar 18, 2022
2 parents ae323e6 + 1d80a33 commit 7265ebe
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 20 deletions.
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ Usage: unifi-protect-backup [OPTIONS]
A Python based tool for backing up Unifi Protect event clips as they occur.
Options:
--version Show the version and exit.
--address TEXT Address of Unifi Protect instance
[required]
--port INTEGER Port of Unifi Protect instance
--port INTEGER Port of Unifi Protect instance [default:
443]
--username TEXT Username to login to Unifi Protect instance
[required]
--password TEXT Password for Unifi Protect user [required]
--verify-ssl / --no-verify-ssl Set if you do not have a valid HTTPS
Certificate for your instance
Certificate for your instance [default:
verify-ssl]
--rclone-destination TEXT `rclone` destination path in the format
{rclone remote}:{path on remote}. E.g.
`gdrive:/backups/unifi_protect` [required]
Expand All @@ -65,15 +68,14 @@ Options:
of `rclone`
(https://rclone.org/filtering/#max-age-don-
t-transfer-any-file-older-than-this)
--rclone-args TEXT Optional arguments which are directly passed
to `rclone rcat`. These can by used to set
parameters such as the bandwidth limit used
when pushing the files to the rclone
destination, e.g., '--bwlimit=500k'. Please
see the `rclone` documentation for the full
set of arguments it supports
(https://rclone.org/docs/). Please use
responsibly.
[default: 7d]
--rclone-args TEXT Optional extra arguments to pass to `rclone
rcat` directly. Common usage for this would
be to set a bandwidth limit, for example.
--detection-types TEXT A comma separated list of which types of
detections to backup. Valid options are:
`motion`, `person`, `vehicle`, `ring`
[default: motion,person,vehicle,ring]
--ignore-camera TEXT IDs of cameras for which events should not
be backed up. Use multiple times to ignore
multiple IDs. If being set as an environment
Expand Down Expand Up @@ -118,6 +120,7 @@ always take priority over environment variables):
- `RCLONE_DESTINATION`
- `RCLONE_ARGS`
- `IGNORE_CAMERAS`
- `DETECTION_TYPES`

## Docker Container
You can run this tool as a container if you prefer with the following command.
Expand All @@ -144,7 +147,7 @@ If you do not already have a `rclone.conf` file you can create one as follows:
```
$ docker run -it --rm -v $PWD:/root/.config/rclone rclone/rclone config
```
Follow the interactive configuration proceed, this will create a `rclone.conf`
Follow the interactive configuration proceed, this will create a `rclone.conf`
file in your current directory.

Finally start the container:
Expand Down
27 changes: 26 additions & 1 deletion unifi_protect_backup/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,31 @@

from unifi_protect_backup import UnifiProtectBackup, __version__

DETECTION_TYPES = ["motion", "person", "vehicle", "ring"]


def _parse_detection_types(ctx, param, value):
# split columns by ',' and remove whitespace
types = [t.strip() for t in value.split(',')]

# validate passed columns
for t in types:
if t not in DETECTION_TYPES:
raise click.BadOptionUsage("detection-types", f"`{t}` is not an available detection type.", ctx)

return types


@click.command()
@click.version_option(__version__)
@click.option('--address', required=True, envvar='UFP_ADDRESS', help='Address of Unifi Protect instance')
@click.option('--port', default=443, envvar='UFP_PORT', help='Port of Unifi Protect instance')
@click.option('--port', default=443, envvar='UFP_PORT', show_default=True, help='Port of Unifi Protect instance')
@click.option('--username', required=True, envvar='UFP_USERNAME', help='Username to login to Unifi Protect instance')
@click.option('--password', required=True, envvar='UFP_PASSWORD', help='Password for Unifi Protect user')
@click.option(
'--verify-ssl/--no-verify-ssl',
default=True,
show_default=True,
envvar='UFP_SSL_VERIFY',
help="Set if you do not have a valid HTTPS Certificate for your instance",
)
Expand All @@ -29,6 +44,7 @@
@click.option(
'--retention',
default='7d',
show_default=True,
envvar='RCLONE_RETENTION',
help="How long should event clips be backed up for. Format as per the `--max-age` argument of "
"`rclone` (https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)",
Expand All @@ -40,6 +56,15 @@
help="Optional extra arguments to pass to `rclone rcat` directly. Common usage for this would "
"be to set a bandwidth limit, for example.",
)
@click.option(
'--detection-types',
envvar='DETECTION_TYPES',
default=','.join(DETECTION_TYPES),
show_default=True,
help="A comma separated list of which types of detections to backup. "
f"Valid options are: {', '.join([f'`{t}`' for t in DETECTION_TYPES])}",
callback=_parse_detection_types,
)
@click.option(
'--ignore-camera',
'ignore_cameras',
Expand Down
36 changes: 29 additions & 7 deletions unifi_protect_backup/unifi_protect_backup.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Main module."""
import asyncio
from datetime import datetime, timedelta, timezone
import json
import logging
import pathlib
import shutil
import json
from asyncio.exceptions import TimeoutError
from datetime import datetime, timedelta, timezone
from typing import Callable, List, Optional

import aiocron
Expand Down Expand Up @@ -175,6 +175,7 @@ class UnifiProtectBackup:
rclone_args (str): Extra args passed directly to `rclone rcat`.
ignore_cameras (List[str]): List of camera IDs for which to not backup events
verbose (int): How verbose to setup logging, see :func:`setup_logging` for details.
detection_types(List[str]): List of which detection types to backup.
_download_queue (asyncio.Queue): Queue of events that need to be backed up
_unsub (Callable): Unsubscribe from the websocket callback
_has_ffprobe (bool): If ffprobe was found on the host
Expand All @@ -189,6 +190,7 @@ def __init__(
rclone_destination: str,
retention: str,
rclone_args: str,
detection_types: List[str],
ignore_cameras: List[str],
verbose: int,
port: int = 443,
Expand All @@ -209,7 +211,8 @@ def __init__(
(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)
rclone_args (str): A bandwidth limit which is passed to the `--bwlimit` argument of
`rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec)
ignore_cameras (List[str]): List of camera IDs for which to not backup events
detection_types (List[str]): List of which detection types to backup.
ignore_cameras (List[str]): List of camera IDs for which to not backup events.
verbose (int): How verbose to setup logging, see :func:`setup_logging` for details.
"""
setup_logging(verbose)
Expand All @@ -229,6 +232,7 @@ def __init__(
logger.debug(f" {rclone_args=}")
logger.debug(f" {ignore_cameras=}")
logger.debug(f" {verbose=}")
logger.debug(f" {detection_types=}")

self.rclone_destination = rclone_destination
self.retention = retention
Expand All @@ -251,6 +255,7 @@ def __init__(
self.ignore_cameras = ignore_cameras
self._download_queue: asyncio.Queue = asyncio.Queue()
self._unsub: Callable[[], None]
self.detection_types = detection_types

self._has_ffprobe = False

Expand Down Expand Up @@ -420,8 +425,20 @@ def _websocket_callback(self, msg: WSSubscriptionMessage) -> None:
return
if msg.new_obj.end is None:
return
if msg.new_obj.type not in {EventType.MOTION, EventType.SMART_DETECT}:
if msg.new_obj.type is EventType.MOTION and "motion" not in self.detection_types:
logger.extra_debug(f"Skipping unwanted motion detection event: {msg.new_obj.id}") # type: ignore
return
if msg.new_obj.type is EventType.RING and "ring" not in self.detection_types:
logger.extra_debug(f"Skipping unwanted ring event: {msg.new_obj.id}") # type: ignore
return
elif msg.new_obj.type is EventType.SMART_DETECT:
for event_smart_detection_type in msg.new_obj.smart_detect_types:
if event_smart_detection_type not in self.detection_types:
logger.extra_debug( # type: ignore
f"Skipping unwanted {event_smart_detection_type} detection event: {msg.new_obj.id}"
)
return

self._download_queue.put_nowait(msg.new_obj)
logger.debug(f"Adding event {msg.new_obj.id} to queue (Current queue={self._download_queue.qsize()})")

Expand All @@ -445,7 +462,10 @@ async def _backup_events(self) -> None:
logger.info(f"Backing up event: {event.id}")
logger.debug(f"Remaining Queue: {self._download_queue.qsize()}")
logger.debug(f" Camera: {await self._get_camera_name(event.camera_id)}")
logger.debug(f" Type: {event.type}")
if event.type == EventType.SMART_DETECT:
logger.debug(f" Type: {event.type} ({', '.join(event.smart_detect_types)})")
else:
logger.debug(f" Type: {event.type}")
logger.debug(f" Start: {event.start.strftime('%Y-%m-%dT%H-%M-%S')} ({event.start.timestamp()})")
logger.debug(f" End: {event.end.strftime('%Y-%m-%dT%H-%M-%S')} ({event.end.timestamp()})")
duration = (event.end - event.start).total_seconds()
Expand Down Expand Up @@ -485,8 +505,10 @@ async def _backup_events(self) -> None:
if self._has_ffprobe:
try:
downloaded_duration = await self._get_video_length(video)
msg = f" Downloaded video length: {downloaded_duration:.3f}s" \
f"({downloaded_duration - duration:+.3f}s)"
msg = (
f" Downloaded video length: {downloaded_duration:.3f}s"
f"({downloaded_duration - duration:+.3f}s)"
)
if downloaded_duration < duration:
logger.warning(msg)
else:
Expand Down

0 comments on commit 7265ebe

Please sign in to comment.