mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-07 03:11:15 +03:00
Profiles fixes (#23306)
* add prop to disable id field * disable id field when editing profile mask/zone also, disable if the zone name already exists in required_zones or the base config is being edited and the id already exists on a profile * add backend validation to reject profile-omly masks/zones * add tests * update docs * tweak
This commit is contained in:
@@ -326,6 +326,47 @@ def verify_required_zones_exist(camera_config: CameraConfig) -> None:
|
||||
)
|
||||
|
||||
|
||||
def verify_profile_overrides_match_base(camera_config: CameraConfig) -> None:
|
||||
"""Verify that profile zone and mask IDs reference entries defined on the base camera."""
|
||||
for profile_name, profile in camera_config.profiles.items():
|
||||
if profile.zones:
|
||||
for zone_name in profile.zones:
|
||||
if zone_name not in camera_config.zones:
|
||||
raise ValueError(
|
||||
f"Camera '{camera_config.name}' profile '{profile_name}' defines "
|
||||
f"zone '{zone_name}' that does not exist on the base config"
|
||||
)
|
||||
|
||||
if profile.motion and profile.motion.mask:
|
||||
for mask_name in profile.motion.mask:
|
||||
if mask_name not in camera_config.motion.mask:
|
||||
raise ValueError(
|
||||
f"Camera '{camera_config.name}' profile '{profile_name}' defines "
|
||||
f"motion mask '{mask_name}' that does not exist on the base config"
|
||||
)
|
||||
|
||||
if profile.objects:
|
||||
for mask_name in profile.objects.mask or {}:
|
||||
if mask_name not in (camera_config.objects.mask or {}):
|
||||
raise ValueError(
|
||||
f"Camera '{camera_config.name}' profile '{profile_name}' defines "
|
||||
f"object mask '{mask_name}' that does not exist on the base config"
|
||||
)
|
||||
for label, filter_config in (profile.objects.filters or {}).items():
|
||||
base_filter = (camera_config.objects.filters or {}).get(label)
|
||||
profile_filter_masks = (
|
||||
filter_config.mask if filter_config else None
|
||||
) or {}
|
||||
base_filter_masks = (base_filter.mask if base_filter else None) or {}
|
||||
for mask_name in profile_filter_masks:
|
||||
if mask_name not in base_filter_masks:
|
||||
raise ValueError(
|
||||
f"Camera '{camera_config.name}' profile '{profile_name}' defines "
|
||||
f"object mask '{mask_name}' for '{label}' that does not exist "
|
||||
f"on the base config"
|
||||
)
|
||||
|
||||
|
||||
def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None:
|
||||
"""Verify that required_zones are specified when autotracking is enabled."""
|
||||
if (
|
||||
@@ -952,6 +993,7 @@ class FrigateConfig(FrigateBaseModel):
|
||||
verify_recording_segments_setup_with_reasonable_time(camera_config)
|
||||
verify_zone_objects_are_tracked(camera_config)
|
||||
verify_required_zones_exist(camera_config)
|
||||
verify_profile_overrides_match_base(camera_config)
|
||||
verify_autotrack_zones(camera_config)
|
||||
verify_motion_and_detect(camera_config)
|
||||
verify_objects_track(camera_config, labelmap_objects)
|
||||
|
||||
@@ -178,6 +178,141 @@ class TestCameraProfileConfig(unittest.TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
FrigateConfig(**config_data)
|
||||
|
||||
def test_profile_zone_without_base_rejected(self):
|
||||
"""Profile defining a zone not present on the base camera is rejected."""
|
||||
from pydantic import ValidationError
|
||||
|
||||
config_data = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"profiles": {
|
||||
"armed": {"friendly_name": "Armed"},
|
||||
},
|
||||
"cameras": {
|
||||
"front": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||
"zones": {
|
||||
"front_yard": {"coordinates": "0,0,100,0,100,100,0,100"},
|
||||
},
|
||||
"profiles": {
|
||||
"armed": {
|
||||
"zones": {
|
||||
"phantom": {
|
||||
"coordinates": "0,0,50,0,50,50,0,50",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
FrigateConfig(**config_data)
|
||||
self.assertIn("phantom", str(ctx.exception))
|
||||
|
||||
def test_profile_motion_mask_without_base_rejected(self):
|
||||
"""Profile defining a motion mask not present on the base camera is rejected."""
|
||||
from pydantic import ValidationError
|
||||
|
||||
config_data = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"profiles": {
|
||||
"armed": {"friendly_name": "Armed"},
|
||||
},
|
||||
"cameras": {
|
||||
"front": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||
"motion": {
|
||||
"mask": {
|
||||
"base_mask": {
|
||||
"coordinates": "0,0,100,0,100,100,0,100",
|
||||
},
|
||||
},
|
||||
},
|
||||
"profiles": {
|
||||
"armed": {
|
||||
"motion": {
|
||||
"mask": {
|
||||
"phantom_mask": {
|
||||
"coordinates": "0,0,50,0,50,50,0,50",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
FrigateConfig(**config_data)
|
||||
self.assertIn("phantom_mask", str(ctx.exception))
|
||||
|
||||
def test_profile_overrides_matching_base_accepted(self):
|
||||
"""Profile overrides that reference existing base zones/masks parse cleanly."""
|
||||
config_data = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"profiles": {
|
||||
"armed": {"friendly_name": "Armed"},
|
||||
},
|
||||
"cameras": {
|
||||
"front": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
}
|
||||
]
|
||||
},
|
||||
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||
"zones": {
|
||||
"front_yard": {"coordinates": "0,0,100,0,100,100,0,100"},
|
||||
},
|
||||
"motion": {
|
||||
"mask": {
|
||||
"tree": {
|
||||
"coordinates": "0,0,100,0,100,100,0,100",
|
||||
},
|
||||
},
|
||||
},
|
||||
"profiles": {
|
||||
"armed": {
|
||||
"zones": {
|
||||
"front_yard": {
|
||||
"coordinates": "0,0,50,0,50,50,0,50",
|
||||
"inertia": 5,
|
||||
},
|
||||
},
|
||||
"motion": {
|
||||
"mask": {
|
||||
"tree": {
|
||||
"coordinates": "0,0,75,0,75,75,0,75",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
config = FrigateConfig(**config_data)
|
||||
assert "armed" in config.cameras["front"].profiles
|
||||
|
||||
|
||||
class TestProfileInConfig(unittest.TestCase):
|
||||
"""Test that profiles parse correctly in FrigateConfig."""
|
||||
|
||||
Reference in New Issue
Block a user