mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 02:29:19 +03:00
add enabled field to camera profiles for enabling/disabling cameras
This commit is contained in:
parent
1b0d78c40e
commit
7f9a782c94
@ -24,6 +24,7 @@ class CameraProfileConfig(FrigateBaseModel):
|
|||||||
explicitly-set fields are used as overrides via exclude_unset.
|
explicitly-set fields are used as overrides via exclude_unset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
enabled: Optional[bool] = None
|
||||||
audio: Optional[AudioConfig] = None
|
audio: Optional[AudioConfig] = None
|
||||||
birdseye: Optional[BirdseyeCameraConfig] = None
|
birdseye: Optional[BirdseyeCameraConfig] = None
|
||||||
detect: Optional[DetectConfig] = None
|
detect: Optional[DetectConfig] = None
|
||||||
|
|||||||
@ -43,12 +43,14 @@ class ProfileManager:
|
|||||||
self.config: FrigateConfig = config
|
self.config: FrigateConfig = config
|
||||||
self.config_updater = config_updater
|
self.config_updater = config_updater
|
||||||
self._base_configs: dict[str, dict[str, dict]] = {}
|
self._base_configs: dict[str, dict[str, dict]] = {}
|
||||||
|
self._base_enabled: dict[str, bool] = {}
|
||||||
self._snapshot_base_configs()
|
self._snapshot_base_configs()
|
||||||
|
|
||||||
def _snapshot_base_configs(self) -> None:
|
def _snapshot_base_configs(self) -> None:
|
||||||
"""Snapshot each camera's current section configs as the base."""
|
"""Snapshot each camera's current section configs and enabled state."""
|
||||||
for cam_name, cam_config in self.config.cameras.items():
|
for cam_name, cam_config in self.config.cameras.items():
|
||||||
self._base_configs[cam_name] = {}
|
self._base_configs[cam_name] = {}
|
||||||
|
self._base_enabled[cam_name] = cam_config.enabled
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
section_config = getattr(cam_config, section, None)
|
section_config = getattr(cam_config, section, None)
|
||||||
if section_config is not None:
|
if section_config is not None:
|
||||||
@ -97,6 +99,13 @@ class ProfileManager:
|
|||||||
def _reset_to_base(self, changed: dict[str, set[str]]) -> None:
|
def _reset_to_base(self, changed: dict[str, set[str]]) -> None:
|
||||||
"""Reset all cameras to their base (no-profile) config."""
|
"""Reset all cameras to their base (no-profile) config."""
|
||||||
for cam_name, cam_config in self.config.cameras.items():
|
for cam_name, cam_config in self.config.cameras.items():
|
||||||
|
# Restore enabled state
|
||||||
|
base_enabled = self._base_enabled.get(cam_name)
|
||||||
|
if base_enabled is not None and cam_config.enabled != base_enabled:
|
||||||
|
cam_config.enabled = base_enabled
|
||||||
|
changed.setdefault(cam_name, set()).add("enabled")
|
||||||
|
|
||||||
|
# Restore section configs
|
||||||
base = self._base_configs.get(cam_name, {})
|
base = self._base_configs.get(cam_name, {})
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
base_data = base.get(section)
|
base_data = base.get(section)
|
||||||
@ -122,6 +131,11 @@ class ProfileManager:
|
|||||||
if profile is None:
|
if profile is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Apply enabled override
|
||||||
|
if profile.enabled is not None and cam_config.enabled != profile.enabled:
|
||||||
|
cam_config.enabled = profile.enabled
|
||||||
|
changed.setdefault(cam_name, set()).add("enabled")
|
||||||
|
|
||||||
base = self._base_configs.get(cam_name, {})
|
base = self._base_configs.get(cam_name, {})
|
||||||
|
|
||||||
for section in PROFILE_SECTION_UPDATES:
|
for section in PROFILE_SECTION_UPDATES:
|
||||||
@ -152,6 +166,15 @@ class ProfileManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for section in sections:
|
for section in sections:
|
||||||
|
if section == "enabled":
|
||||||
|
self.config_updater.publish_update(
|
||||||
|
CameraConfigUpdateTopic(
|
||||||
|
CameraConfigUpdateEnum.enabled, cam_name
|
||||||
|
),
|
||||||
|
cam_config.enabled,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
update_enum = PROFILE_SECTION_UPDATES.get(section)
|
update_enum = PROFILE_SECTION_UPDATES.get(section)
|
||||||
if update_enum is None:
|
if update_enum is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -53,6 +53,23 @@ class TestCameraProfileConfig(unittest.TestCase):
|
|||||||
assert profile.review is not None
|
assert profile.review is not None
|
||||||
assert profile.review.alerts.labels == ["person", "car"]
|
assert profile.review.alerts.labels == ["person", "car"]
|
||||||
|
|
||||||
|
def test_enabled_field(self):
|
||||||
|
"""Profile with enabled set to False."""
|
||||||
|
profile = CameraProfileConfig(enabled=False)
|
||||||
|
assert profile.enabled is False
|
||||||
|
dumped = profile.model_dump(exclude_unset=True)
|
||||||
|
assert dumped == {"enabled": False}
|
||||||
|
|
||||||
|
def test_enabled_field_true(self):
|
||||||
|
"""Profile with enabled set to True."""
|
||||||
|
profile = CameraProfileConfig(enabled=True)
|
||||||
|
assert profile.enabled is True
|
||||||
|
|
||||||
|
def test_enabled_default_none(self):
|
||||||
|
"""Enabled defaults to None when not set."""
|
||||||
|
profile = CameraProfileConfig()
|
||||||
|
assert profile.enabled is None
|
||||||
|
|
||||||
def test_none_sections_not_in_dump(self):
|
def test_none_sections_not_in_dump(self):
|
||||||
"""Sections left as None should not appear in exclude_unset dump."""
|
"""Sections left as None should not appear in exclude_unset dump."""
|
||||||
profile = CameraProfileConfig(detect={"enabled": False})
|
profile = CameraProfileConfig(detect={"enabled": False})
|
||||||
@ -330,6 +347,66 @@ class TestProfileManager(unittest.TestCase):
|
|||||||
# Back camera has no "disarmed" profile, should be unchanged
|
# Back camera has no "disarmed" profile, should be unchanged
|
||||||
assert self.config.cameras["back"].notifications.enabled == back_base_notifications
|
assert self.config.cameras["back"].notifications.enabled == back_base_notifications
|
||||||
|
|
||||||
|
@patch.object(ProfileManager, "_persist_active_profile")
|
||||||
|
def test_activate_profile_disables_camera(self, mock_persist):
|
||||||
|
"""Profile with enabled=false disables the camera."""
|
||||||
|
# Add a profile that disables the front camera
|
||||||
|
from frigate.config.camera.profile import CameraProfileConfig
|
||||||
|
|
||||||
|
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||||
|
enabled=False
|
||||||
|
)
|
||||||
|
# Re-create manager to pick up new profile
|
||||||
|
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||||
|
|
||||||
|
assert self.config.cameras["front"].enabled is True
|
||||||
|
err = self.manager.activate_profile("away")
|
||||||
|
assert err is None
|
||||||
|
assert self.config.cameras["front"].enabled is False
|
||||||
|
|
||||||
|
@patch.object(ProfileManager, "_persist_active_profile")
|
||||||
|
def test_deactivate_restores_enabled(self, mock_persist):
|
||||||
|
"""Deactivating a profile restores the camera's base enabled state."""
|
||||||
|
from frigate.config.camera.profile import CameraProfileConfig
|
||||||
|
|
||||||
|
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||||
|
enabled=False
|
||||||
|
)
|
||||||
|
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||||
|
|
||||||
|
self.manager.activate_profile("away")
|
||||||
|
assert self.config.cameras["front"].enabled is False
|
||||||
|
|
||||||
|
self.manager.activate_profile(None)
|
||||||
|
assert self.config.cameras["front"].enabled is True
|
||||||
|
|
||||||
|
@patch.object(ProfileManager, "_persist_active_profile")
|
||||||
|
def test_enabled_zmq_published(self, mock_persist):
|
||||||
|
"""ZMQ update is published for enabled state change."""
|
||||||
|
from frigate.config.camera.profile import CameraProfileConfig
|
||||||
|
from frigate.config.camera.updater import (
|
||||||
|
CameraConfigUpdateEnum,
|
||||||
|
CameraConfigUpdateTopic,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.config.cameras["front"].profiles["away"] = CameraProfileConfig(
|
||||||
|
enabled=False
|
||||||
|
)
|
||||||
|
self.manager = ProfileManager(self.config, self.mock_updater)
|
||||||
|
self.mock_updater.reset_mock()
|
||||||
|
|
||||||
|
self.manager.activate_profile("away")
|
||||||
|
|
||||||
|
# Find the enabled update call
|
||||||
|
enabled_calls = [
|
||||||
|
call
|
||||||
|
for call in self.mock_updater.publish_update.call_args_list
|
||||||
|
if call[0][0]
|
||||||
|
== CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, "front")
|
||||||
|
]
|
||||||
|
assert len(enabled_calls) == 1
|
||||||
|
assert enabled_calls[0][0][1] is False
|
||||||
|
|
||||||
@patch.object(ProfileManager, "_persist_active_profile")
|
@patch.object(ProfileManager, "_persist_active_profile")
|
||||||
def test_zmq_updates_published(self, mock_persist):
|
def test_zmq_updates_published(self, mock_persist):
|
||||||
"""ZMQ updates are published when a profile is activated."""
|
"""ZMQ updates are published when a profile is activated."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user