Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More config checks #4310

Merged
merged 6 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 69 additions & 39 deletions frigate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
create_mask,
deep_merge,
escape_special_characters,
load_config_with_no_duplicates,
load_labels,
)

Expand Down Expand Up @@ -786,6 +787,66 @@ class LoggerConfig(FrigateBaseModel):
)


def verify_config_roles(camera_config: CameraConfig) -> None:
"""Verify that roles are setup in the config correctly."""
assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
)

if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError(
f"Camera {camera_config.name} has record enabled, but record is not assigned to an input."
)

if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
raise ValueError(
f"Camera {camera_config.name} has rtmp enabled, but rtmp is not assigned to an input."
)

if camera_config.restream.enabled and not "restream" in assigned_roles:
raise ValueError(
f"Camera {camera_config.name} has restream enabled, but restream is not assigned to an input."
)


def verify_old_retain_config(camera_config: CameraConfig) -> None:
"""Leave log if old retain_days is used."""
if not camera_config.record.retain_days is None:
logger.warning(
"The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
)
if camera_config.record.retain.days == 0:
camera_config.record.retain.days = camera_config.record.retain_days


def verify_recording_retention(camera_config: CameraConfig) -> None:
"""Verify that recording retention modes are ranked correctly."""
rank_map = {
RetainModeEnum.all: 0,
RetainModeEnum.motion: 1,
RetainModeEnum.active_objects: 2,
}

if (
camera_config.record.retain.days != 0
and rank_map[camera_config.record.retain.mode]
> rank_map[camera_config.record.events.retain.mode]
):
logger.warning(
f"{camera_config.name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
)


def verify_zone_objects_are_tracked(camera_config: CameraConfig) -> None:
"""Verify that user has not entered zone objects that are not in the tracking config."""
for zone_name, zone in camera_config.zones.items():
for obj in zone.objects:
if obj not in camera_config.objects.track:
raise ValueError(
f"Zone {zone_name} is configured to track {obj} but that object type is not added to objects -> track."
)


class FrigateConfig(FrigateBaseModel):
mqtt: MqttConfig = Field(title="MQTT Configuration.")
database: DatabaseConfig = Field(
Expand Down Expand Up @@ -927,47 +988,16 @@ def runtime_config(self) -> FrigateConfig:
**camera_config.motion.dict(exclude_unset=True),
)

# check runtime config
assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
)
if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError(
f"Camera {name} has record enabled, but record is not assigned to an input."
)

if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
raise ValueError(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
verify_config_roles(camera_config)
verify_old_retain_config(camera_config)
verify_recording_retention(camera_config)
verify_zone_objects_are_tracked(camera_config)

if camera_config.restream.enabled and not "restream" in assigned_roles:
raise ValueError(
f"Camera {name} has restream enabled, but restream is not assigned to an input."
)

# backwards compatibility for retain_days
if not camera_config.record.retain_days is None:
if camera_config.rtmp.enabled:
logger.warning(
"The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
)
if camera_config.record.retain.days == 0:
camera_config.record.retain.days = camera_config.record.retain_days

# warning if the higher level record mode is potentially more restrictive than the events
rank_map = {
RetainModeEnum.all: 0,
RetainModeEnum.motion: 1,
RetainModeEnum.active_objects: 2,
}
if (
camera_config.record.retain.days != 0
and rank_map[camera_config.record.retain.mode]
> rank_map[camera_config.record.events.retain.mode]
):
logger.warning(
f"{name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
"RTMP restream is deprecated in favor of the restream role, recommend disabling RTMP."
)

# generate the ffmpeg commands
camera_config.create_ffmpeg_cmds()
config.cameras[name] = camera_config
Expand All @@ -987,7 +1017,7 @@ def parse_file(cls, config_file):
raw_config = f.read()

if config_file.endswith(YAML_EXT):
config = yaml.safe_load(raw_config)
config = load_config_with_no_duplicates(raw_config)
elif config_file.endswith(".json"):
config = json.loads(raw_config)

Expand Down
47 changes: 47 additions & 0 deletions frigate/test/test_config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import unittest
import numpy as np
from pydantic import ValidationError

from frigate.config import (
BirdseyeModeEnum,
FrigateConfig,
DetectorTypeEnum,
)
from frigate.util import load_config_with_no_duplicates


class TestConfig(unittest.TestCase):
Expand Down Expand Up @@ -1424,6 +1426,51 @@ def test_fails_on_bad_camera_name(self):
ValidationError, lambda: frigate_config.runtime_config.cameras
)

def test_fails_zone_defines_untracked_object(self):
config = {
"mqtt": {"host": "mqtt"},
"objects": {"track": ["person"]},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
"zones": {
"steps": {
"coordinates": "0,0,0,0",
"objects": ["car", "person"],
},
},
}
},
}

frigate_config = FrigateConfig(**config)

self.assertRaises(ValueError, lambda: frigate_config.runtime_config.cameras)

def test_fails_duplicate_keys(self):
raw_config = """
cameras:
test:
ffmpeg:
inputs:
- one
- two
inputs:
- three
- four
"""

self.assertRaises(
ValueError, lambda: load_config_with_no_duplicates(raw_config)
)

def test_object_filter_ratios_work(self):
config = {
"mqtt": {"host": "mqtt"},
Expand Down
30 changes: 30 additions & 0 deletions frigate/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import signal
import traceback
import urllib.parse
import yaml

from abc import ABC, abstractmethod
from collections import Counter
from collections.abc import Mapping
from multiprocessing import shared_memory
from typing import AnyStr
Expand Down Expand Up @@ -44,6 +47,33 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic
return merged


def load_config_with_no_duplicates(raw_config) -> dict:
"""Get config ensuring duplicate keys are not allowed."""

# https://stackoverflow.com/a/71751051
class PreserveDuplicatesLoader(yaml.loader.Loader):
pass

def map_constructor(loader, node, deep=False):
keys = [loader.construct_object(node, deep=deep) for node, _ in node.value]
vals = [loader.construct_object(node, deep=deep) for _, node in node.value]
key_count = Counter(keys)
data = {}
for key, val in zip(keys, vals):
if key_count[key] > 1:
raise ValueError(
f"Config input {key} is defined multiple times for the same field, this is not allowed."
)
else:
data[key] = val
return data

PreserveDuplicatesLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, map_constructor
)
return yaml.load(raw_config, PreserveDuplicatesLoader)


def draw_timestamp(
frame,
timestamp,
Expand Down