mirror of
https://github.com/ep1cman/unifi-protect-backup.git
synced 2025-12-05 23:53:30 +00:00
Compare commits
6 Commits
restructur
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5372deaf8 | ||
|
|
ee2dd587e1 | ||
|
|
7265ebe177 | ||
|
|
1d80a330e5 | ||
|
|
649041f590 | ||
|
|
883eb5c133 |
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.5.3
|
||||
current_version = 0.6.0
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.6.0] - 2022-03-18
|
||||
### Added
|
||||
- Support for doorbell ring events
|
||||
- `detection_types` parameter to limit which kinds of events are backed up
|
||||
### Fixed
|
||||
- Actually fixed timestamps this time.
|
||||
|
||||
## [0.5.3] - 2022-03-11
|
||||
### Fixed
|
||||
- Timestamps in filenames and logging now show time in the timezone of the NVR not UTC
|
||||
|
||||
@@ -25,7 +25,7 @@ RUN \
|
||||
|
||||
# Install unifi-protect-backup
|
||||
RUN echo "**** install unifi-protect-backup ****"
|
||||
COPY dist/unifi-protect-backup-0.5.3.tar.gz sdist.tar.gz
|
||||
COPY dist/unifi-protect-backup-0.6.0.tar.gz sdist.tar.gz
|
||||
RUN \
|
||||
pip install sdist.tar.gz && \
|
||||
echo "**** cleanup ****" && \
|
||||
|
||||
27
README.md
27
README.md
@@ -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`, `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
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[tool]
|
||||
[tool.poetry]
|
||||
name = "unifi-protect-backup"
|
||||
version = "0.5.3"
|
||||
version = "0.6.0"
|
||||
homepage = "https://github.com/ep1cman/unifi-protect-backup"
|
||||
description = "Python tool to backup unifi event clips in realtime."
|
||||
authors = ["sebastian.goscik <sebastian@goscik.com>"]
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
__author__ = """sebastian.goscik"""
|
||||
__email__ = 'sebastian@goscik.com'
|
||||
__version__ = '0.5.3'
|
||||
__version__ = '0.6.0'
|
||||
|
||||
from .unifi_protect_backup import UnifiProtectBackup
|
||||
|
||||
@@ -6,16 +6,31 @@ import click
|
||||
|
||||
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",
|
||||
)
|
||||
@@ -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,15 @@ 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="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',
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -209,7 +211,8 @@ class UnifiProtectBackup:
|
||||
(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)
|
||||
@@ -229,6 +232,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 +255,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
|
||||
|
||||
@@ -420,8 +425,20 @@ class UnifiProtectBackup:
|
||||
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()})")
|
||||
|
||||
@@ -445,7 +462,10 @@ 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)}")
|
||||
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()
|
||||
@@ -485,8 +505,10 @@ class UnifiProtectBackup:
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user