mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-02 00:07:11 +01:00
More config checks (#4310)
* Move existing checks to own functions * Add config check for zone objects that are not tracked * Add tests for config error * Formatting * Catch case where field is defined multiple times and add test * Add warning for rtmp
This commit is contained in:
parent
8665a24560
commit
9e31873520
@ -22,6 +22,7 @@ from frigate.util import (
|
|||||||
create_mask,
|
create_mask,
|
||||||
deep_merge,
|
deep_merge,
|
||||||
escape_special_characters,
|
escape_special_characters,
|
||||||
|
load_config_with_no_duplicates,
|
||||||
load_labels,
|
load_labels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -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):
|
class FrigateConfig(FrigateBaseModel):
|
||||||
mqtt: MqttConfig = Field(title="MQTT Configuration.")
|
mqtt: MqttConfig = Field(title="MQTT Configuration.")
|
||||||
database: DatabaseConfig = Field(
|
database: DatabaseConfig = Field(
|
||||||
@ -927,47 +988,16 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
**camera_config.motion.dict(exclude_unset=True),
|
**camera_config.motion.dict(exclude_unset=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# check runtime config
|
verify_config_roles(camera_config)
|
||||||
assigned_roles = list(
|
verify_old_retain_config(camera_config)
|
||||||
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
|
verify_recording_retention(camera_config)
|
||||||
)
|
verify_zone_objects_are_tracked(camera_config)
|
||||||
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:
|
if camera_config.rtmp.enabled:
|
||||||
raise ValueError(
|
|
||||||
f"Camera {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 {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:
|
|
||||||
logger.warning(
|
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'"
|
"RTMP restream is deprecated in favor of the restream role, recommend disabling RTMP."
|
||||||
)
|
)
|
||||||
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."
|
|
||||||
)
|
|
||||||
# generate the ffmpeg commands
|
# generate the ffmpeg commands
|
||||||
camera_config.create_ffmpeg_cmds()
|
camera_config.create_ffmpeg_cmds()
|
||||||
config.cameras[name] = camera_config
|
config.cameras[name] = camera_config
|
||||||
@ -987,7 +1017,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
raw_config = f.read()
|
raw_config = f.read()
|
||||||
|
|
||||||
if config_file.endswith(YAML_EXT):
|
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"):
|
elif config_file.endswith(".json"):
|
||||||
config = json.loads(raw_config)
|
config = json.loads(raw_config)
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from frigate.config import (
|
from frigate.config import (
|
||||||
BirdseyeModeEnum,
|
BirdseyeModeEnum,
|
||||||
FrigateConfig,
|
FrigateConfig,
|
||||||
DetectorTypeEnum,
|
DetectorTypeEnum,
|
||||||
)
|
)
|
||||||
|
from frigate.util import load_config_with_no_duplicates
|
||||||
|
|
||||||
|
|
||||||
class TestConfig(unittest.TestCase):
|
class TestConfig(unittest.TestCase):
|
||||||
@ -1424,6 +1426,51 @@ class TestConfig(unittest.TestCase):
|
|||||||
ValidationError, lambda: frigate_config.runtime_config.cameras
|
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):
|
def test_object_filter_ratios_work(self):
|
||||||
config = {
|
config = {
|
||||||
"mqtt": {"host": "mqtt"},
|
"mqtt": {"host": "mqtt"},
|
||||||
|
@ -5,7 +5,10 @@ import re
|
|||||||
import signal
|
import signal
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
import yaml
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import Counter
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from multiprocessing import shared_memory
|
from multiprocessing import shared_memory
|
||||||
from typing import AnyStr
|
from typing import AnyStr
|
||||||
@ -44,6 +47,33 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic
|
|||||||
return merged
|
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(
|
def draw_timestamp(
|
||||||
frame,
|
frame,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
Loading…
Reference in New Issue
Block a user