mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 02:29:19 +03:00
add zones support to camera profiles
This commit is contained in:
parent
d15fc4e58e
commit
60930e50c2
@ -12,6 +12,7 @@ from .objects import ObjectConfig
|
||||
from .record import RecordConfig
|
||||
from .review import ReviewConfig
|
||||
from .snapshots import SnapshotsConfig
|
||||
from .zone import ZoneConfig
|
||||
|
||||
__all__ = ["CameraProfileConfig"]
|
||||
|
||||
@ -34,3 +35,4 @@ class CameraProfileConfig(FrigateBaseModel):
|
||||
record: Optional[RecordConfig] = None
|
||||
review: Optional[ReviewConfig] = None
|
||||
snapshots: Optional[SnapshotsConfig] = None
|
||||
zones: Optional[dict[str, ZoneConfig]] = None
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""Profile manager for activating/deactivating named config profiles."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@ -9,6 +10,7 @@ from frigate.config.camera.updater import (
|
||||
CameraConfigUpdatePublisher,
|
||||
CameraConfigUpdateTopic,
|
||||
)
|
||||
from frigate.config.camera.zone import ZoneConfig
|
||||
from frigate.const import CONFIG_DIR
|
||||
from frigate.util.builtin import deep_merge
|
||||
from frigate.util.config import apply_section_update
|
||||
@ -44,13 +46,15 @@ class ProfileManager:
|
||||
self.config_updater = config_updater
|
||||
self._base_configs: dict[str, dict[str, dict]] = {}
|
||||
self._base_enabled: dict[str, bool] = {}
|
||||
self._base_zones: dict[str, dict[str, ZoneConfig]] = {}
|
||||
self._snapshot_base_configs()
|
||||
|
||||
def _snapshot_base_configs(self) -> None:
|
||||
"""Snapshot each camera's current section configs and enabled state."""
|
||||
"""Snapshot each camera's current section configs, enabled, and zones."""
|
||||
for cam_name, cam_config in self.config.cameras.items():
|
||||
self._base_configs[cam_name] = {}
|
||||
self._base_enabled[cam_name] = cam_config.enabled
|
||||
self._base_zones[cam_name] = copy.deepcopy(cam_config.zones)
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
section_config = getattr(cam_config, section, None)
|
||||
if section_config is not None:
|
||||
@ -105,6 +109,12 @@ class ProfileManager:
|
||||
cam_config.enabled = base_enabled
|
||||
changed.setdefault(cam_name, set()).add("enabled")
|
||||
|
||||
# Restore zones
|
||||
base_zones = self._base_zones.get(cam_name)
|
||||
if base_zones is not None and cam_config.zones != base_zones:
|
||||
cam_config.zones = copy.deepcopy(base_zones)
|
||||
changed.setdefault(cam_name, set()).add("zones")
|
||||
|
||||
# Restore section configs
|
||||
base = self._base_configs.get(cam_name, {})
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
@ -136,6 +146,14 @@ class ProfileManager:
|
||||
cam_config.enabled = profile.enabled
|
||||
changed.setdefault(cam_name, set()).add("enabled")
|
||||
|
||||
# Apply zones override — merge profile zones into base zones
|
||||
if profile.zones is not None:
|
||||
base_zones = self._base_zones.get(cam_name, {})
|
||||
merged_zones = copy.deepcopy(base_zones)
|
||||
merged_zones.update(profile.zones)
|
||||
cam_config.zones = merged_zones
|
||||
changed.setdefault(cam_name, set()).add("zones")
|
||||
|
||||
base = self._base_configs.get(cam_name, {})
|
||||
|
||||
for section in PROFILE_SECTION_UPDATES:
|
||||
@ -175,6 +193,15 @@ class ProfileManager:
|
||||
)
|
||||
continue
|
||||
|
||||
if section == "zones":
|
||||
self.config_updater.publish_update(
|
||||
CameraConfigUpdateTopic(
|
||||
CameraConfigUpdateEnum.zones, cam_name
|
||||
),
|
||||
cam_config.zones,
|
||||
)
|
||||
continue
|
||||
|
||||
update_enum = PROFILE_SECTION_UPDATES.get(section)
|
||||
if update_enum is None:
|
||||
continue
|
||||
|
||||
@ -70,6 +70,24 @@ class TestCameraProfileConfig(unittest.TestCase):
|
||||
profile = CameraProfileConfig()
|
||||
assert profile.enabled is None
|
||||
|
||||
def test_zones_field(self):
|
||||
"""Profile with zones override."""
|
||||
profile = CameraProfileConfig(
|
||||
zones={
|
||||
"driveway": {
|
||||
"coordinates": "0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9",
|
||||
"objects": ["car"],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert profile.zones is not None
|
||||
assert "driveway" in profile.zones
|
||||
|
||||
def test_zones_default_none(self):
|
||||
"""Zones defaults to None when not set."""
|
||||
profile = CameraProfileConfig()
|
||||
assert profile.zones is None
|
||||
|
||||
def test_none_sections_not_in_dump(self):
|
||||
"""Sections left as None should not appear in exclude_unset dump."""
|
||||
profile = CameraProfileConfig(detect={"enabled": False})
|
||||
@ -380,6 +398,81 @@ class TestProfileManager(unittest.TestCase):
|
||||
self.manager.activate_profile(None)
|
||||
assert self.config.cameras["front"].enabled is True
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_activate_profile_adds_zone(self, mock_persist):
|
||||
"""Profile with zones adds/overrides zones on camera."""
|
||||
from frigate.config.camera.profile import CameraProfileConfig
|
||||
from frigate.config.camera.zone import ZoneConfig
|
||||
|
||||
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||
zones={
|
||||
"driveway": ZoneConfig(
|
||||
coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9",
|
||||
objects=["car"],
|
||||
)
|
||||
}
|
||||
)
|
||||
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||
|
||||
assert "driveway" not in self.config.cameras["front"].zones
|
||||
|
||||
err = self.manager.activate_profile("away")
|
||||
assert err is None
|
||||
assert "driveway" in self.config.cameras["front"].zones
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_deactivate_restores_zones(self, mock_persist):
|
||||
"""Deactivating a profile restores base zones."""
|
||||
from frigate.config.camera.profile import CameraProfileConfig
|
||||
from frigate.config.camera.zone import ZoneConfig
|
||||
|
||||
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||
zones={
|
||||
"driveway": ZoneConfig(
|
||||
coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9",
|
||||
objects=["car"],
|
||||
)
|
||||
}
|
||||
)
|
||||
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||
|
||||
self.manager.activate_profile("away")
|
||||
assert "driveway" in self.config.cameras["front"].zones
|
||||
|
||||
self.manager.activate_profile(None)
|
||||
assert "driveway" not in self.config.cameras["front"].zones
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_zones_zmq_published(self, mock_persist):
|
||||
"""ZMQ update is published for zones change."""
|
||||
from frigate.config.camera.profile import CameraProfileConfig
|
||||
from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateEnum,
|
||||
CameraConfigUpdateTopic,
|
||||
)
|
||||
from frigate.config.camera.zone import ZoneConfig
|
||||
|
||||
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||
zones={
|
||||
"driveway": ZoneConfig(
|
||||
coordinates="0.1,0.1,0.9,0.1,0.9,0.9,0.1,0.9",
|
||||
objects=["car"],
|
||||
)
|
||||
}
|
||||
)
|
||||
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||
self.mock_updater.reset_mock()
|
||||
|
||||
self.manager.activate_profile("away")
|
||||
|
||||
zones_calls = [
|
||||
call
|
||||
for call in self.mock_updater.publish_update.call_args_list
|
||||
if call[0][0]
|
||||
== CameraConfigUpdateTopic(CameraConfigUpdateEnum.zones, "front")
|
||||
]
|
||||
assert len(zones_calls) == 1
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_enabled_zmq_published(self, mock_persist):
|
||||
"""ZMQ update is published for enabled state change."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user