diff --git a/frigate/api/app.py b/frigate/api/app.py index cb408b447e..09a459156a 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -34,7 +34,6 @@ from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryPa from frigate.api.defs.request.app_body import ( AppConfigSetBody, MediaSyncBody, - ProfileSetBody, ) from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig @@ -244,24 +243,6 @@ def get_active_profile(request: Request): return JSONResponse(content={"active_profile": config_obj.active_profile}) -@router.put("/profile/set", dependencies=[Depends(require_role(["admin"]))]) -def set_profile(request: Request, body: ProfileSetBody): - """Activate or deactivate a profile.""" - profile_manager = request.app.profile_manager - err = profile_manager.activate_profile(body.profile) - if err: - return JSONResponse( - content={"success": False, "message": err}, - status_code=400, - ) - request.app.dispatcher.publish("profile/state", body.profile or "none", retain=True) - return JSONResponse( - content={ - "success": True, - "active_profile": body.profile, - } - ) - @router.get("/ffmpeg/presets", dependencies=[Depends(allow_any_authenticated())]) def ffmpeg_presets(): diff --git a/frigate/api/camera.py b/frigate/api/camera.py index cb69a56e3b..c99126e64e 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -23,6 +23,7 @@ from frigate.api.auth import ( require_camera_access, require_role, ) +from frigate.api.defs.request.app_body import CameraSetBody from frigate.api.defs.tags import Tags from frigate.config import FrigateConfig from frigate.config.camera.updater import ( @@ -1155,3 +1156,76 @@ async def delete_camera( }, status_code=200, ) + + +_SUB_COMMAND_FEATURES = {"motion_mask", "object_mask", "zone"} + + +@router.put( + "/camera/{camera_name}/set/{feature}", + dependencies=[Depends(require_role(["admin"]))], +) +@router.put( + "/camera/{camera_name}/set/{feature}/{sub_command}", + dependencies=[Depends(require_role(["admin"]))], +) +def camera_set( + request: Request, + camera_name: str, + feature: str, + body: CameraSetBody, + sub_command: str | None = None, +): + """Set a camera feature state. Use camera_name='*' to target all cameras.""" + dispatcher = request.app.dispatcher + frigate_config: FrigateConfig = request.app.frigate_config + + if feature == "profile": + if camera_name != "*": + return JSONResponse( + content={ + "success": False, + "message": "Profile feature requires camera_name='*'", + }, + status_code=400, + ) + dispatcher._receive("profile/set", body.value) + return JSONResponse(content={"success": True}) + + if feature not in dispatcher._camera_settings_handlers: + return JSONResponse( + content={"success": False, "message": f"Unknown feature: {feature}"}, + status_code=400, + ) + + if sub_command and feature not in _SUB_COMMAND_FEATURES: + return JSONResponse( + content={ + "success": False, + "message": f"Feature '{feature}' does not support sub-commands", + }, + status_code=400, + ) + + if camera_name == "*": + cameras = list(frigate_config.cameras.keys()) + elif camera_name not in frigate_config.cameras: + return JSONResponse( + content={ + "success": False, + "message": f"Camera '{camera_name}' not found", + }, + status_code=404, + ) + else: + cameras = [camera_name] + + for cam in cameras: + topic = ( + f"{cam}/{feature}/{sub_command}/set" + if sub_command + else f"{cam}/{feature}/set" + ) + dispatcher._receive(topic, body.value) + + return JSONResponse(content={"success": True}) diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 1640da739a..45392a1380 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -30,10 +30,8 @@ class AppPutRoleBody(BaseModel): role: str -class ProfileSetBody(BaseModel): - profile: Optional[str] = Field( - default=None, description="Profile name to activate, or null to deactivate" - ) +class CameraSetBody(BaseModel): + value: str = Field(..., description="The value to set for the feature") class MediaSyncBody(BaseModel): diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index 7353a30359..dfe39be470 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -128,7 +128,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { const handleActivateProfile = async (profileName: string | null) => { try { - await axios.put("profile/set", { profile: profileName || null }); + await axios.put("camera/*/set/profile", { value: profileName ?? "none" }); await updateProfiles(); toast.success( profileName diff --git a/web/src/views/settings/ProfilesView.tsx b/web/src/views/settings/ProfilesView.tsx index b9c00b6d5d..3c44360156 100644 --- a/web/src/views/settings/ProfilesView.tsx +++ b/web/src/views/settings/ProfilesView.tsx @@ -207,8 +207,8 @@ export default function ProfilesView({ async (profile: string | null) => { setActivating(true); try { - await axios.put("profile/set", { - profile: profile || null, + await axios.put("camera/*/set/profile", { + value: profile ?? "none", }); await updateProfiles(); toast.success( @@ -244,7 +244,7 @@ export default function ProfilesView({ try { // If this profile is active, deactivate it first if (activeProfile === deleteProfile) { - await axios.put("profile/set", { profile: null }); + await axios.put("camera/*/set/profile", { value: "none" }); } // Remove the profile from all cameras and the top-level definition