From d749cf2e6b0e21463e2caa13302262d57f5077ba Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 15 Apr 2022 05:59:30 -0600 Subject: [PATCH 1/4] Allow birdseye to be overridden at the camera level (#3083) * Add camera level processing for birdseye * Add camera level birdseye configruation * Propogate birdseye from global * Update docs to show that birdseye is overridable * Fix incorrect default factory * Update note to indicate values that can be overridden * Cleanup config accessing * Add tests for birdseye config behavior * Fix mistake on test format * Update tests --- docs/docs/configuration/index.md | 1 + frigate/config.py | 11 ++++++ frigate/output.py | 14 +++++--- frigate/test/test_config.py | 57 ++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index a6ab0985f..28be554d5 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -110,6 +110,7 @@ environment_vars: EXAMPLE_VAR: value # Optional: birdseye configuration +# NOTE: Can (enabled, mode) be overridden at the camera level birdseye: # Optional: Enable birdseye view (default: shown below) enabled: True diff --git a/frigate/config.py b/frigate/config.py index 026e93c73..c3830c4af 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -324,6 +324,13 @@ class BirdseyeConfig(FrigateBaseModel): ) +class BirdseyeCameraConfig(FrigateBaseModel): + enabled: bool = Field(default=True, title="Enable birdseye view for camera.") + mode: BirdseyeModeEnum = Field( + default=BirdseyeModeEnum.objects, title="Tracking mode for camera." + ) + + FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"] FFMPEG_INPUT_ARGS_DEFAULT = [ "-avoid_negative_ts", @@ -539,6 +546,9 @@ class CameraConfig(FrigateBaseModel): detect: DetectConfig = Field( default_factory=DetectConfig, title="Object detection configuration." ) + birdseye: BirdseyeCameraConfig = Field( + default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration." + ) timestamp_style: TimestampStyleConfig = Field( default_factory=TimestampStyleConfig, title="Timestamp style configuration." ) @@ -775,6 +785,7 @@ class FrigateConfig(FrigateBaseModel): # Global config to propegate down to camera level global_config = config.dict( include={ + "birdseye": ..., "record": ..., "snapshots": ..., "live": ..., diff --git a/frigate/output.py b/frigate/output.py index 62c672172..4676dac83 100644 --- a/frigate/output.py +++ b/frigate/output.py @@ -190,14 +190,14 @@ class BirdsEyeFrameManager: channel_dims, ) - def camera_active(self, object_box_count, motion_box_count): - if self.mode == BirdseyeModeEnum.continuous: + def camera_active(self, mode, object_box_count, motion_box_count): + if mode == BirdseyeModeEnum.continuous: return True - if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0: + if mode == BirdseyeModeEnum.motion and motion_box_count > 0: return True - if self.mode == BirdseyeModeEnum.objects and object_box_count > 0: + if mode == BirdseyeModeEnum.objects and object_box_count > 0: return True def update_frame(self): @@ -311,10 +311,14 @@ class BirdsEyeFrameManager: return True def update(self, camera, object_count, motion_count, frame_time, frame) -> bool: + # don't process if birdseye is disabled for this camera + camera_config = self.config.cameras[camera].birdseye + if not camera_config.enabled: + return False # update the last active frame for the camera self.cameras[camera]["current_frame"] = frame_time - if self.camera_active(object_count, motion_count): + if self.camera_active(camera_config.mode, object_count, motion_count): self.cameras[camera]["last_active_frame"] = frame_time now = datetime.datetime.now().timestamp() diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 2e30fab4b..83bd94901 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -2,6 +2,7 @@ import unittest import numpy as np from pydantic import ValidationError from frigate.config import ( + BirdseyeModeEnum, FrigateConfig, DetectorTypeEnum, ) @@ -80,6 +81,62 @@ class TestConfig(unittest.TestCase): runtime_config = frigate_config.runtime_config assert "dog" in runtime_config.cameras["back"].objects.track + def test_override_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": { "enabled": True, "mode": "continuous" }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "birdseye": { + "enabled": False, + "mode": "motion" + }, + } + }, + } + frigate_config = FrigateConfig(**config) + assert config == frigate_config.dict(exclude_unset=True) + + runtime_config = frigate_config.runtime_config + assert not runtime_config.cameras["back"].birdseye.enabled + assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion + + def test_inherit_birdseye(self): + config = { + "mqtt": {"host": "mqtt"}, + "birdseye": { "enabled": True, "mode": "continuous" }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + frigate_config = FrigateConfig(**config) + assert config == frigate_config.dict(exclude_unset=True) + + runtime_config = frigate_config.runtime_config + assert runtime_config.cameras["back"].birdseye.enabled + assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous + def test_override_tracked_objects(self): config = { "mqtt": {"host": "mqtt"}, From d9957614199a127c31a62417f79715f92690f1b5 Mon Sep 17 00:00:00 2001 From: Sebastian Englbrecht Date: Tue, 12 Apr 2022 15:30:55 +0200 Subject: [PATCH 2/4] Prepare mypy for typing checks --- .github/workflows/pull_request.yml | 4 +++- Makefile | 14 +++----------- docker/Dockerfile | 2 +- frigate/mypy.ini | 6 ++++++ requirements-wheels.txt | 1 + 5 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 frigate/mypy.ini diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fe9e76b8b..6985ece9b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -56,5 +56,7 @@ jobs: uses: docker/setup-buildx-action@v1 - name: Build run: make + - name: Run mypy + run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate - name: Run tests - run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest + run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest \ No newline at end of file diff --git a/Makefile b/Makefile index 3c3f8c04a..2f308c845 100644 --- a/Makefile +++ b/Makefile @@ -14,16 +14,8 @@ frigate: version frigate_push: version docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:0.11.0-$(COMMIT_HASH) --file docker/Dockerfile . -run_tests: - # PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7 - # ARCH: aarch64 amd64 or armv7 - @cat docker/Dockerfile.base docker/Dockerfile.$(ARCH) > docker/Dockerfile.test - @sed -i "s/FROM frigate-web as web/#/g" docker/Dockerfile.test - @sed -i "s/COPY --from=web \/opt\/frigate\/build web\//#/g" docker/Dockerfile.test - @sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test - @echo "" >> docker/Dockerfile.test - @echo "RUN python3 -m unittest" >> docker/Dockerfile.test - @docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test . - @rm docker/Dockerfile.test +run_tests: frigate + docker run --rm --entrypoint=python3 frigate:latest -u -m unittest + docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate .PHONY: run_tests diff --git a/docker/Dockerfile b/docker/Dockerfile index cd755aaf7..ee988a2a2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,7 +42,7 @@ COPY requirements.txt /requirements.txt RUN pip3 install -r requirements.txt COPY requirements-wheels.txt /requirements-wheels.txt -RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt +RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt # Frigate Container FROM debian:11-slim diff --git a/frigate/mypy.ini b/frigate/mypy.ini new file mode 100644 index 000000000..c6f93381d --- /dev/null +++ b/frigate/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +python_version = 3.9 +ignore_missing_imports = true + +[mypy-frigate.*] +ignore_errors = true diff --git a/requirements-wheels.txt b/requirements-wheels.txt index a817c318c..94786bf53 100644 --- a/requirements-wheels.txt +++ b/requirements-wheels.txt @@ -2,6 +2,7 @@ click == 8.1.* Flask == 2.1.* imutils == 0.5.* matplotlib == 3.5.* +mypy == 0.942 numpy == 1.22.* opencv-python-headless == 4.5.5.* paho-mqtt == 1.6.* From c6234bf54832ede96cb9039c758af2a59eeb709d Mon Sep 17 00:00:00 2001 From: Sebastian Englbrecht Date: Tue, 12 Apr 2022 20:22:43 +0200 Subject: [PATCH 3/4] fix depreciated import from collections --- frigate/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frigate/util.py b/frigate/util.py index 621de3cbb..49bda8620 100755 --- a/frigate/util.py +++ b/frigate/util.py @@ -1,4 +1,3 @@ -import collections import copy import datetime import hashlib @@ -11,6 +10,7 @@ import threading import time import traceback from abc import ABC, abstractmethod +from collections.abc import Mapping from multiprocessing import shared_memory from typing import AnyStr @@ -34,7 +34,7 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic for k, v2 in dct2.items(): if k in merged: v1 = merged[k] - if isinstance(v1, dict) and isinstance(v2, collections.Mapping): + if isinstance(v1, dict) and isinstance(v2, Mapping): merged[k] = deep_merge(v1, v2, override) elif isinstance(v1, list) and isinstance(v2, list): if merge_lists: From 41f58c76927458efc11db8277292c962ba599337 Mon Sep 17 00:00:00 2001 From: Sebastian Englbrecht Date: Tue, 12 Apr 2022 22:24:45 +0200 Subject: [PATCH 4/4] Add basic typing for multiple modules: * log.py * video.py * watchdog.py * zeroconf.py --- frigate/log.py | 27 +++++++++++++-------------- frigate/mypy.ini | 34 ++++++++++++++++++++++++++++++++++ frigate/video.py | 5 ++--- frigate/watchdog.py | 16 ++++++++++------ frigate/zeroconf.py | 27 +++++++++++++++------------ 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/frigate/log.py b/frigate/log.py index efe7d554a..e28eee6f0 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -4,13 +4,14 @@ import threading import os import signal import queue -import multiprocessing as mp +from multiprocessing.queues import Queue from logging import handlers from setproctitle import setproctitle +from typing import Deque from collections import deque -def listener_configurer(): +def listener_configurer() -> None: root = logging.getLogger() console_handler = logging.StreamHandler() formatter = logging.Formatter( @@ -21,14 +22,14 @@ def listener_configurer(): root.setLevel(logging.INFO) -def root_configurer(queue): +def root_configurer(queue: Queue) -> None: h = handlers.QueueHandler(queue) root = logging.getLogger() root.addHandler(h) root.setLevel(logging.INFO) -def log_process(log_queue): +def log_process(log_queue: Queue) -> None: threading.current_thread().name = f"logger" setproctitle("frigate.logger") listener_configurer() @@ -43,34 +44,32 @@ def log_process(log_queue): # based on https://codereview.stackexchange.com/a/17959 class LogPipe(threading.Thread): - def __init__(self, log_name, level): - """Setup the object with a logger and a loglevel - and start the thread - """ + def __init__(self, log_name: str): + """Setup the object with a logger and start the thread""" threading.Thread.__init__(self) self.daemon = False self.logger = logging.getLogger(log_name) - self.level = level - self.deque = deque(maxlen=100) + self.level = logging.ERROR + self.deque: Deque[str] = deque(maxlen=100) self.fdRead, self.fdWrite = os.pipe() self.pipeReader = os.fdopen(self.fdRead) self.start() - def fileno(self): + def fileno(self) -> int: """Return the write file descriptor of the pipe""" return self.fdWrite - def run(self): + def run(self) -> None: """Run the thread, logging everything.""" for line in iter(self.pipeReader.readline, ""): self.deque.append(line.strip("\n")) self.pipeReader.close() - def dump(self): + def dump(self) -> None: while len(self.deque) > 0: self.logger.log(self.level, self.deque.popleft()) - def close(self): + def close(self) -> None: """Close the write end of the pipe.""" os.close(self.fdWrite) diff --git a/frigate/mypy.ini b/frigate/mypy.ini index c6f93381d..d8c34b966 100644 --- a/frigate/mypy.ini +++ b/frigate/mypy.ini @@ -1,6 +1,40 @@ [mypy] python_version = 3.9 +show_error_codes = true +follow_imports = silent ignore_missing_imports = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +enable_error_code = ignore-without-code +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true +no_implicit_reexport = true [mypy-frigate.*] ignore_errors = true + +[mypy-frigate.const] +ignore_errors = false + +[mypy-frigate.log] +ignore_errors = false + +[mypy-frigate.version] +ignore_errors = false + +[mypy-frigate.watchdog] +ignore_errors = false +disallow_untyped_calls = false + +[mypy-frigate.zeroconf] +ignore_errors = false diff --git a/frigate/video.py b/frigate/video.py index cea749ec5..93639c72f 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -203,7 +203,7 @@ class CameraWatchdog(threading.Thread): self.config = config self.capture_thread = None self.ffmpeg_detect_process = None - self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR) + self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect") self.ffmpeg_other_processes = [] self.camera_fps = camera_fps self.ffmpeg_pid = ffmpeg_pid @@ -219,8 +219,7 @@ class CameraWatchdog(threading.Thread): if "detect" in c["roles"]: continue logpipe = LogPipe( - f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}", - logging.ERROR, + f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}" ) self.ffmpeg_other_processes.append( { diff --git a/frigate/watchdog.py b/frigate/watchdog.py index 73cf86240..f593ae066 100644 --- a/frigate/watchdog.py +++ b/frigate/watchdog.py @@ -5,21 +5,22 @@ import time import os import signal -from frigate.util import ( - restart_frigate, -) +from frigate.edgetpu import EdgeTPUProcess +from frigate.util import restart_frigate +from multiprocessing.synchronize import Event +from typing import Dict logger = logging.getLogger(__name__) class FrigateWatchdog(threading.Thread): - def __init__(self, detectors, stop_event): + def __init__(self, detectors: Dict[str, EdgeTPUProcess], stop_event: Event): threading.Thread.__init__(self) self.name = "frigate_watchdog" self.detectors = detectors self.stop_event = stop_event - def run(self): + def run(self) -> None: time.sleep(10) while not self.stop_event.wait(10): now = datetime.datetime.now().timestamp() @@ -32,7 +33,10 @@ class FrigateWatchdog(threading.Thread): "Detection appears to be stuck. Restarting detection process..." ) detector.start_or_restart() - elif not detector.detect_process.is_alive(): + elif ( + detector.detect_process is not None + and not detector.detect_process.is_alive() + ): logger.info("Detection appears to have stopped. Exiting frigate...") restart_frigate() diff --git a/frigate/zeroconf.py b/frigate/zeroconf.py index 8007a33ab..c9c20aba6 100644 --- a/frigate/zeroconf.py +++ b/frigate/zeroconf.py @@ -14,38 +14,41 @@ logger = logging.getLogger(__name__) ZEROCONF_TYPE = "_frigate._tcp.local." # Taken from: http://stackoverflow.com/a/11735897 -def get_local_ip() -> str: +def get_local_ip() -> bytes: """Try to determine the local IP address of the machine.""" + host_ip_str = "" try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Use Google Public DNS server to determine own IP sock.connect(("8.8.8.8", 80)) - return sock.getsockname()[0] # type: ignore + host_ip_str = sock.getsockname()[0] except OSError: try: - return socket.gethostbyname(socket.gethostname()) + host_ip_str = socket.gethostbyname(socket.gethostname()) except socket.gaierror: - return "127.0.0.1" + host_ip_str = "127.0.0.1" finally: sock.close() + try: + host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip_str) + except OSError: + host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip_str) -def broadcast_zeroconf(frigate_id): + return host_ip_pton + + +def broadcast_zeroconf(frigate_id: str) -> Zeroconf: zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only) host_ip = get_local_ip() - try: - host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip) - except OSError: - host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip) - info = ServiceInfo( ZEROCONF_TYPE, name=f"{frigate_id}.{ZEROCONF_TYPE}", - addresses=[host_ip_pton], + addresses=[host_ip], port=5000, ) @@ -56,4 +59,4 @@ def broadcast_zeroconf(frigate_id): logger.error( "Frigate instance with identical name present in the local network" ) - return zeroconf \ No newline at end of file + return zeroconf