add enabled field to camera profiles for enabling/disabling cameras

This commit is contained in:
Josh Hawkins 2026-03-06 15:47:17 -06:00
parent 7c6926d1e6
commit d15fc4e58e
3 changed files with 102 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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."""