mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-31 07:27:57 +00:00
fix the model lookup KeyError for cameras added at runtime (#24026)
This commit is contained in:
parent
fec73c887e
commit
a94b532655
@ -96,6 +96,7 @@ class CameraConfigUpdateSubscriber:
|
||||
return
|
||||
elif update_type == CameraConfigUpdateEnum.remove:
|
||||
self.config.cameras.pop(camera, None)
|
||||
self.config.drop_camera_model(camera)
|
||||
self.camera_configs.pop(camera, None)
|
||||
return
|
||||
|
||||
|
||||
@ -687,13 +687,37 @@ class FrigateConfig(FrigateBaseModel):
|
||||
def model_for_camera(self, camera_name: str) -> ModelConfig:
|
||||
"""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:
|
||||
camera_name: Name of the camera
|
||||
|
||||
Returns:
|
||||
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]:
|
||||
"""Get the parsed hardware devices a model runs on.
|
||||
|
||||
@ -9,6 +9,32 @@ from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateSubscriber,
|
||||
)
|
||||
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:
|
||||
@ -86,6 +112,32 @@ class TestRecordUpdateRecreatesFfmpegCmds(unittest.TestCase):
|
||||
|
||||
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):
|
||||
camera_config = _build_camera_config(sub_enabled=False)
|
||||
subscriber = CameraConfigUpdateSubscriber(
|
||||
|
||||
@ -179,6 +179,50 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
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")
|
||||
def test_camera_scene_without_a_model_or_a_default(self, mock_labels):
|
||||
mock_labels.return_value = {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user