Added ability to choose which event types to backup

Co-authored-by: J3n50m4t <j3n50m4t@j3n50m4t.com>
This commit is contained in:
Sebastian Goscik
2022-03-18 20:12:44 +00:00
parent ae323e68aa
commit 453fed6c57
3 changed files with 59 additions and 14 deletions

View File

@@ -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]
@@ -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` [default:
motion,person,vehicle]
--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
@@ -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.

View File

@@ -6,16 +6,31 @@ import click
from unifi_protect_backup import UnifiProtectBackup, __version__
DETECTION_TYPES = ["motion", "person", "vehicle"]
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",
)
@@ -29,6 +44,7 @@ from unifi_protect_backup import UnifiProtectBackup, __version__
@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)",
@@ -40,6 +56,14 @@ from unifi_protect_backup import UnifiProtectBackup, __version__
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=f"A comma separated list of which types of detections to backup. Valid options are: {', '.join([f'`{t}`' for t in DETECTION_TYPES])}",
callback=_parse_detection_types,
)
@click.option(
'--ignore-camera',
'ignore_cameras',

View File

@@ -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
@@ -189,6 +190,7 @@ class UnifiProtectBackup:
rclone_destination: str,
retention: str,
rclone_args: str,
detection_types: List[str],
ignore_cameras: List[str],
verbose: int,
port: int = 443,
@@ -229,6 +231,7 @@ class UnifiProtectBackup:
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
@@ -251,6 +254,7 @@ class UnifiProtectBackup:
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
@@ -422,6 +426,15 @@ class UnifiProtectBackup:
return
if msg.new_obj.type not in {EventType.MOTION, EventType.SMART_DETECT}:
return
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}")
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(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()})")
@@ -445,7 +458,12 @@ class UnifiProtectBackup:
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)}")
if event.type == EventType.MOTION:
logger.debug(f" Type: {event.type}")
elif event.type == EventType.SMART_DETECT:
logger.debug(f" Type: {event.type} ({', '.join(event.smart_detect_types)})")
else:
ValueError(f"Unexpected event 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()