Refactor Birdseye activity types as composable booleans (#23940)

* Add combined motion and object Birdseye mode

Add a motion_objects mode that keeps Birdseye active when motion is detected or a confirmed tracked object is present, including stationary objects.

Wire the mode through configuration, runtime commands, API schemas, documentation, and UI labels. Exclude false-positive trackers and add regression coverage for Birdseye activation and MQTT validation.

* Refactor Birdseye activity types as booleans

Replace combination-specific Birdseye modes with composable boolean activity types for motion, active objects, stationary objects, and continuous display.

Preserve legacy single-mode configuration and MQTT inputs, support canonical comma-separated MQTT combinations, and allow scalar YAML values to be replaced by nested settings through the config API.

* Preserve OpenVINO config translations

Regenerate the configuration translations with the OpenVINO detector schema available so the unrelated production detector labels remain intact.

* Preserve partial Birdseye mode overrides

Allow an empty activity selection with a canonical NONE MQTT state so partial camera and profile overrides can disable inherited flags without failing validation.

Add regression coverage for camera and profile inheritance, document the NONE contract, and keep the generated schema fixture scoped to Birdseye.

* Address Birdseye activity review feedback

Move scalar mode compatibility into the 0.18-1 config migration and reject empty activity selections instead of publishing a NONE state.

Pass activity signals through a frozen dataclass, preserve existing active-object tracker behavior, and require confirmed stationary objects. Revert the generic YAML mutation and cover migration, inheritance, MQTT, and activation regressions.

* Move Birdseye migration to 0.19

Use the 0.19-0 configuration revision for converting scalar Birdseye modes to composable activity flags, and update the migration regression coverage accordingly.

* Remove Birdseye migration test

Drop the dedicated config migration test as requested during review while retaining the 0.19-0 migration implementation.
This commit is contained in:
Ersa Oktavian Ramadan
2026-08-22 11:40:42 -05:00
committed by Josh Hawkins
parent a9d09f8a81
commit 4147d01374
22 changed files with 606 additions and 108 deletions
+100 -9
View File
@@ -7,7 +7,7 @@ import numpy as np
from pydantic import ValidationError
from ruamel.yaml.constructor import DuplicateKeyError
from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.config import FrigateConfig
from frigate.const import MODEL_CACHE_DIR
from frigate.detectors import DetectorTypeEnum
from frigate.util.builtin import deep_merge
@@ -170,7 +170,7 @@ class TestConfig(unittest.TestCase):
def test_override_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"birdseye": {"enabled": True, "mode": {"continuous": True}},
"cameras": {
"back": {
"ffmpeg": {
@@ -183,19 +183,30 @@ class TestConfig(unittest.TestCase):
"width": 1920,
"fps": 5,
},
"birdseye": {"enabled": False, "mode": "motion"},
"birdseye": {
"enabled": False,
"mode": {"continuous": False, "motion": True},
},
}
},
}
frigate_config = FrigateConfig(**config)
assert not frigate_config.cameras["back"].birdseye.enabled
assert frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
mode = frigate_config.cameras["back"].birdseye.mode
assert mode.motion
assert not mode.continuous
assert not mode.objects
assert not mode.stationary_objects
def test_override_birdseye_non_inheritable(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous", "height": 1920},
"birdseye": {
"enabled": True,
"mode": {"continuous": True},
"height": 1920,
},
"cameras": {
"back": {
"ffmpeg": {
@@ -218,7 +229,7 @@ class TestConfig(unittest.TestCase):
def test_inherit_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": {"enabled": True, "mode": "continuous"},
"birdseye": {"enabled": True, "mode": {"continuous": True}},
"cameras": {
"back": {
"ffmpeg": {
@@ -237,9 +248,89 @@ class TestConfig(unittest.TestCase):
frigate_config = FrigateConfig(**config)
assert frigate_config.cameras["back"].birdseye.enabled
assert (
frigate_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
)
mode = frigate_config.cameras["back"].birdseye.mode
assert mode.continuous
assert not mode.motion
assert not mode.objects
assert not mode.stationary_objects
def test_combine_birdseye_activity_types(self):
config = {
**self.minimal,
"birdseye": {
"mode": {
"motion": True,
"stationary_objects": True,
}
},
}
frigate_config = FrigateConfig(**config)
mode = frigate_config.cameras["back"].birdseye.mode
assert mode.motion
assert mode.stationary_objects
assert not mode.continuous
assert not mode.objects
def test_birdseye_requires_an_activity_type(self):
config = {
**self.minimal,
"birdseye": {
"mode": {
"continuous": False,
"motion": False,
"objects": False,
"stationary_objects": False,
}
},
}
with self.assertRaisesRegex(
ValidationError, "must enable at least one Birdseye activity type"
):
FrigateConfig(**config)
def test_camera_can_disable_an_inherited_activity_type(self):
config = {
**self.minimal,
"birdseye": {"mode": {"motion": True, "objects": True}},
}
config["cameras"]["back"]["birdseye"] = {"mode": {"motion": False}}
frigate_config = FrigateConfig(**config)
mode = frigate_config.cameras["back"].birdseye.mode
assert not mode.motion
assert mode.objects
def test_profile_must_leave_an_activity_type_enabled(self):
config = {
**self.minimal,
"profiles": {"away": {"friendly_name": "Away"}},
"birdseye": {"mode": {"objects": True}},
}
config["cameras"]["back"]["profiles"] = {
"away": {"birdseye": {"mode": {"objects": False}}}
}
with self.assertRaisesRegex(
ValidationError, "must enable at least one Birdseye activity type"
):
FrigateConfig(**config)
def test_camera_birdseye_activity_types_override_global_values(self):
config = {
**self.minimal,
"birdseye": {"mode": {"motion": True, "objects": True}},
}
config["cameras"]["back"]["birdseye"] = {
"mode": {"motion": False, "stationary_objects": True}
}
frigate_config = FrigateConfig(**config)
mode = frigate_config.cameras["back"].birdseye.mode
assert not mode.motion
assert mode.objects
assert mode.stationary_objects
def test_override_tracked_objects(self):
config = {