mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-02 04:46:50 +03:00
fix the model lookup KeyError for cameras added at runtime (#24026)
This commit is contained in:
committed by
Nicolas Mowen
parent
257a05a7e2
commit
46796fe9ef
@@ -96,6 +96,7 @@ class CameraConfigUpdateSubscriber:
|
|||||||
return
|
return
|
||||||
elif update_type == CameraConfigUpdateEnum.remove:
|
elif update_type == CameraConfigUpdateEnum.remove:
|
||||||
self.config.cameras.pop(camera, None)
|
self.config.cameras.pop(camera, None)
|
||||||
|
self.config.drop_camera_model(camera)
|
||||||
self.camera_configs.pop(camera, None)
|
self.camera_configs.pop(camera, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -687,13 +687,37 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
def model_for_camera(self, camera_name: str) -> ModelConfig:
|
def model_for_camera(self, camera_name: str) -> ModelConfig:
|
||||||
"""Get the detection model a camera runs on.
|
"""Get the detection model a camera runs on.
|
||||||
|
|
||||||
|
Cameras added at runtime (wizard, clone, debug replay) are inserted
|
||||||
|
into cameras after parse, so they miss the cache built during
|
||||||
|
post_validation and are resolved here on first lookup.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
camera_name: Name of the camera
|
camera_name: Name of the camera
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The model matching the camera's detect scene
|
The model matching the camera's detect scene
|
||||||
"""
|
"""
|
||||||
return self._camera_models[camera_name]
|
model = self._camera_models.get(camera_name)
|
||||||
|
|
||||||
|
if model is None:
|
||||||
|
camera = self.cameras.get(camera_name)
|
||||||
|
scene = camera.detect.scene if camera is not None else SceneEnum.all
|
||||||
|
model = self._resolve_camera_model(camera_name, scene)
|
||||||
|
self._camera_models[camera_name] = model
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
def drop_camera_model(self, camera_name: str) -> None:
|
||||||
|
"""Forget the cached model for a camera removed at runtime.
|
||||||
|
|
||||||
|
A later re-add resolves fresh, so a camera recreated under the same
|
||||||
|
name with a different detect scene doesn't inherit the removed
|
||||||
|
camera's model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_name: Name of the removed camera
|
||||||
|
"""
|
||||||
|
self._camera_models.pop(camera_name, None)
|
||||||
|
|
||||||
def devices_for_model(self, model: ModelConfig) -> list[DeviceSpec]:
|
def devices_for_model(self, model: ModelConfig) -> list[DeviceSpec]:
|
||||||
"""Get the parsed hardware devices a model runs on.
|
"""Get the parsed hardware devices a model runs on.
|
||||||
|
|||||||
@@ -9,6 +9,32 @@ from frigate.config.camera.updater import (
|
|||||||
CameraConfigUpdateSubscriber,
|
CameraConfigUpdateSubscriber,
|
||||||
)
|
)
|
||||||
from frigate.const import SUB_CACHE_TAG
|
from frigate.const import SUB_CACHE_TAG
|
||||||
|
from frigate.detectors.detector_config import SceneEnum
|
||||||
|
|
||||||
|
|
||||||
|
def _build_scene_frigate_config(scene: str | None) -> FrigateConfig:
|
||||||
|
detect = {"height": 1080, "width": 1920, "fps": 5}
|
||||||
|
if scene is not None:
|
||||||
|
detect["scene"] = scene
|
||||||
|
return FrigateConfig(
|
||||||
|
**{
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"models": [
|
||||||
|
{"devices": ["cpu"]},
|
||||||
|
{"scene": "outdoor", "devices": ["openvino:CPU"]},
|
||||||
|
],
|
||||||
|
"cameras": {
|
||||||
|
"front_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": detect,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_camera_config(sub_enabled: bool) -> CameraConfig:
|
def _build_camera_config(sub_enabled: bool) -> CameraConfig:
|
||||||
@@ -86,6 +112,32 @@ class TestRecordUpdateRecreatesFfmpegCmds(unittest.TestCase):
|
|||||||
|
|
||||||
assert not _has_sub_output(camera_config)
|
assert not _has_sub_output(camera_config)
|
||||||
|
|
||||||
|
@patch("frigate.detectors.detector_config.load_labels")
|
||||||
|
def test_removed_camera_readded_without_scene_gets_fresh_model(self, mock_labels):
|
||||||
|
mock_labels.return_value = {}
|
||||||
|
config = _build_scene_frigate_config("outdoor")
|
||||||
|
subscriber = CameraConfigUpdateSubscriber(
|
||||||
|
config, {}, [CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.remove]
|
||||||
|
)
|
||||||
|
assert config.model_for_camera("front_door").scene == SceneEnum.outdoor
|
||||||
|
|
||||||
|
subscriber.subscriber.check_for_update.side_effect = [
|
||||||
|
("config/cameras/front_door/remove", config.cameras["front_door"]),
|
||||||
|
(None, None),
|
||||||
|
]
|
||||||
|
subscriber.check_for_updates()
|
||||||
|
|
||||||
|
# recreating the camera through the wizard leaves the scene unset,
|
||||||
|
# so the removed camera's cached model must not carry over
|
||||||
|
readded = _build_scene_frigate_config(None).cameras["front_door"]
|
||||||
|
subscriber.subscriber.check_for_update.side_effect = [
|
||||||
|
("config/cameras/front_door/add", readded),
|
||||||
|
(None, None),
|
||||||
|
]
|
||||||
|
subscriber.check_for_updates()
|
||||||
|
|
||||||
|
assert config.model_for_camera("front_door").scene == SceneEnum.all
|
||||||
|
|
||||||
def test_unchanged_record_update_keeps_existing_cmds(self):
|
def test_unchanged_record_update_keeps_existing_cmds(self):
|
||||||
camera_config = _build_camera_config(sub_enabled=False)
|
camera_config = _build_camera_config(sub_enabled=False)
|
||||||
subscriber = CameraConfigUpdateSubscriber(
|
subscriber = CameraConfigUpdateSubscriber(
|
||||||
|
|||||||
@@ -179,6 +179,50 @@ class TestConfig(unittest.TestCase):
|
|||||||
|
|
||||||
assert frigate_config.model_for_camera("back").scene == SceneEnum.all
|
assert frigate_config.model_for_camera("back").scene == SceneEnum.all
|
||||||
|
|
||||||
|
@patch("frigate.detectors.detector_config.load_labels")
|
||||||
|
def test_model_for_camera_resolves_camera_added_after_parse(self, mock_labels):
|
||||||
|
mock_labels.return_value = {}
|
||||||
|
config = {
|
||||||
|
"models": [
|
||||||
|
{"devices": ["cpu"], "width": 320},
|
||||||
|
{"scene": "outdoor", "devices": ["openvino:CPU"], "width": 416},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate_config = FrigateConfig(**(deep_merge(deepcopy(config), self.minimal)))
|
||||||
|
|
||||||
|
# runtime camera adds (wizard, clone, debug replay) insert an already
|
||||||
|
# resolved camera into the shared config without re-running parse
|
||||||
|
added = deepcopy(self.minimal)
|
||||||
|
added["cameras"]["new_cam"] = {
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5, "scene": "outdoor"},
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
new_config = FrigateConfig(**(deep_merge(deepcopy(config), added)))
|
||||||
|
frigate_config.cameras["new_cam"] = new_config.cameras["new_cam"]
|
||||||
|
|
||||||
|
assert frigate_config.model_for_camera("new_cam").scene == SceneEnum.outdoor
|
||||||
|
assert frigate_config.model_for_camera("new_cam").width == 416
|
||||||
|
|
||||||
|
@patch("frigate.detectors.detector_config.load_labels")
|
||||||
|
def test_model_for_camera_unknown_camera_uses_default_model(self, mock_labels):
|
||||||
|
mock_labels.return_value = {}
|
||||||
|
config = {
|
||||||
|
"models": [
|
||||||
|
{"devices": ["cpu"], "width": 320},
|
||||||
|
{"scene": "outdoor", "devices": ["openvino:CPU"], "width": 416},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate_config = FrigateConfig(**(deep_merge(deepcopy(config), self.minimal)))
|
||||||
|
|
||||||
|
# a caller racing a runtime remove may still name the popped camera
|
||||||
|
assert frigate_config.model_for_camera("removed").scene == SceneEnum.all
|
||||||
|
|
||||||
@patch("frigate.detectors.detector_config.load_labels")
|
@patch("frigate.detectors.detector_config.load_labels")
|
||||||
def test_camera_scene_without_a_model_or_a_default(self, mock_labels):
|
def test_camera_scene_without_a_model_or_a_default(self, mock_labels):
|
||||||
mock_labels.return_value = {}
|
mock_labels.return_value = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user