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. A Python based tool for backing up Unifi Protect event clips as they occur.
Options: Options:
--version Show the version and exit.
--address TEXT Address of Unifi Protect instance --address TEXT Address of Unifi Protect instance
[required] [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 --username TEXT Username to login to Unifi Protect instance
[required] [required]
--password TEXT Password for Unifi Protect user [required] --password TEXT Password for Unifi Protect user [required]
--verify-ssl / --no-verify-ssl Set if you do not have a valid HTTPS --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-destination TEXT `rclone` destination path in the format
{rclone remote}:{path on remote}. E.g. {rclone remote}:{path on remote}. E.g.
`gdrive:/backups/unifi_protect` [required] `gdrive:/backups/unifi_protect` [required]
@@ -65,15 +68,14 @@ Options:
of `rclone` of `rclone`
(https://rclone.org/filtering/#max-age-don- (https://rclone.org/filtering/#max-age-don-
t-transfer-any-file-older-than-this) t-transfer-any-file-older-than-this)
--rclone-args TEXT Optional arguments which are directly passed [default: 7d]
to `rclone rcat`. These can by used to set --rclone-args TEXT Optional extra arguments to pass to `rclone
parameters such as the bandwidth limit used rcat` directly. Common usage for this would
when pushing the files to the rclone be to set a bandwidth limit, for example.
destination, e.g., '--bwlimit=500k'. Please --detection-types TEXT A comma separated list of which types of
see the `rclone` documentation for the full detections to backup. Valid options are:
set of arguments it supports `motion`, `person`, `vehicle` [default:
(https://rclone.org/docs/). Please use motion,person,vehicle]
responsibly.
--ignore-camera TEXT IDs of cameras for which events should not --ignore-camera TEXT IDs of cameras for which events should not
be backed up. Use multiple times to ignore be backed up. Use multiple times to ignore
multiple IDs. If being set as an environment multiple IDs. If being set as an environment
@@ -118,6 +120,7 @@ always take priority over environment variables):
- `RCLONE_DESTINATION` - `RCLONE_DESTINATION`
- `RCLONE_ARGS` - `RCLONE_ARGS`
- `IGNORE_CAMERAS` - `IGNORE_CAMERAS`
- `DETECTION_TYPES`
## Docker Container ## Docker Container
You can run this tool as a container if you prefer with the following command. You can run this tool as a container if you prefer with the following command.
@@ -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 $ 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. file in your current directory.
Finally start the container: Finally start the container:

View File

@@ -6,16 +6,31 @@ import click
from unifi_protect_backup import UnifiProtectBackup, __version__ 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.command()
@click.version_option(__version__) @click.version_option(__version__)
@click.option('--address', required=True, envvar='UFP_ADDRESS', help='Address of Unifi Protect instance') @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('--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('--password', required=True, envvar='UFP_PASSWORD', help='Password for Unifi Protect user')
@click.option( @click.option(
'--verify-ssl/--no-verify-ssl', '--verify-ssl/--no-verify-ssl',
default=True, default=True,
show_default=True,
envvar='UFP_SSL_VERIFY', envvar='UFP_SSL_VERIFY',
help="Set if you do not have a valid HTTPS Certificate for your instance", 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( @click.option(
'--retention', '--retention',
default='7d', default='7d',
show_default=True,
envvar='RCLONE_RETENTION', envvar='RCLONE_RETENTION',
help="How long should event clips be backed up for. Format as per the `--max-age` argument of " 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)", "`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 " help="Optional extra arguments to pass to `rclone rcat` directly. Common usage for this would "
"be to set a bandwidth limit, for example.", "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( @click.option(
'--ignore-camera', '--ignore-camera',
'ignore_cameras', 'ignore_cameras',

View File

@@ -175,6 +175,7 @@ class UnifiProtectBackup:
rclone_args (str): Extra args passed directly to `rclone rcat`. rclone_args (str): Extra args passed directly to `rclone rcat`.
ignore_cameras (List[str]): List of camera IDs for which to not backup events 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. 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 _download_queue (asyncio.Queue): Queue of events that need to be backed up
_unsub (Callable): Unsubscribe from the websocket callback _unsub (Callable): Unsubscribe from the websocket callback
_has_ffprobe (bool): If ffprobe was found on the host _has_ffprobe (bool): If ffprobe was found on the host
@@ -189,6 +190,7 @@ class UnifiProtectBackup:
rclone_destination: str, rclone_destination: str,
retention: str, retention: str,
rclone_args: str, rclone_args: str,
detection_types: List[str],
ignore_cameras: List[str], ignore_cameras: List[str],
verbose: int, verbose: int,
port: int = 443, port: int = 443,
@@ -229,6 +231,7 @@ class UnifiProtectBackup:
logger.debug(f" {rclone_args=}") logger.debug(f" {rclone_args=}")
logger.debug(f" {ignore_cameras=}") logger.debug(f" {ignore_cameras=}")
logger.debug(f" {verbose=}") logger.debug(f" {verbose=}")
logger.debug(f" {detection_types=}")
self.rclone_destination = rclone_destination self.rclone_destination = rclone_destination
self.retention = retention self.retention = retention
@@ -251,6 +254,7 @@ class UnifiProtectBackup:
self.ignore_cameras = ignore_cameras self.ignore_cameras = ignore_cameras
self._download_queue: asyncio.Queue = asyncio.Queue() self._download_queue: asyncio.Queue = asyncio.Queue()
self._unsub: Callable[[], None] self._unsub: Callable[[], None]
self.detection_types = detection_types
self._has_ffprobe = False self._has_ffprobe = False
@@ -422,6 +426,15 @@ class UnifiProtectBackup:
return return
if msg.new_obj.type not in {EventType.MOTION, EventType.SMART_DETECT}: if msg.new_obj.type not in {EventType.MOTION, EventType.SMART_DETECT}:
return 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) 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()})") 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.info(f"Backing up event: {event.id}")
logger.debug(f"Remaining Queue: {self._download_queue.qsize()}") logger.debug(f"Remaining Queue: {self._download_queue.qsize()}")
logger.debug(f" Camera: {await self._get_camera_name(event.camera_id)}") logger.debug(f" Camera: {await self._get_camera_name(event.camera_id)}")
logger.debug(f" Type: {event.type}") 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" 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()})") logger.debug(f" End: {event.end.strftime('%Y-%m-%dT%H-%M-%S')} ({event.end.timestamp()})")
duration = (event.end - event.start).total_seconds() duration = (event.end - event.start).total_seconds()