mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-10 07:25:27 +03:00
Compare commits
No commits in common. "cedcbdba078cb32305ce5553254ce09565b6f3ca" and "a9a2eecebb532bc4e759b391920384c07cc1a936" have entirely different histories.
cedcbdba07
...
a9a2eecebb
@ -11,8 +11,7 @@ These are the MQTT messages generated by Frigate. The default topic_prefix is `f
|
|||||||
|
|
||||||
Designed to be used as an availability topic with Home Assistant. Possible message are:
|
Designed to be used as an availability topic with Home Assistant. Possible message are:
|
||||||
"online": published when Frigate is running (on startup)
|
"online": published when Frigate is running (on startup)
|
||||||
"stopped": published when Frigate is stopped normally
|
"offline": published after Frigate has stopped
|
||||||
"offline": published automatically by the MQTT broker if Frigate disconnects unexpectedly (via MQTT Will Message)
|
|
||||||
|
|
||||||
### `frigate/restart`
|
### `frigate/restart`
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryPa
|
|||||||
from frigate.api.defs.request.app_body import (
|
from frigate.api.defs.request.app_body import (
|
||||||
AppConfigSetBody,
|
AppConfigSetBody,
|
||||||
MediaSyncBody,
|
MediaSyncBody,
|
||||||
|
ProfileSetBody,
|
||||||
)
|
)
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
@ -243,6 +244,25 @@ def get_active_profile(request: Request):
|
|||||||
return JSONResponse(content={"active_profile": config_obj.active_profile})
|
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())])
|
@router.get("/ffmpeg/presets", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def ffmpeg_presets():
|
def ffmpeg_presets():
|
||||||
"""Return available ffmpeg preset keys for config UI usage."""
|
"""Return available ffmpeg preset keys for config UI usage."""
|
||||||
|
|||||||
@ -23,7 +23,6 @@ from frigate.api.auth import (
|
|||||||
require_camera_access,
|
require_camera_access,
|
||||||
require_role,
|
require_role,
|
||||||
)
|
)
|
||||||
from frigate.api.defs.request.app_body import CameraSetBody
|
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.config.camera.updater import (
|
from frigate.config.camera.updater import (
|
||||||
@ -1156,76 +1155,3 @@ async def delete_camera(
|
|||||||
},
|
},
|
||||||
status_code=200,
|
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})
|
|
||||||
|
|||||||
@ -140,62 +140,6 @@ def get_tool_definitions() -> List[Dict[str, Any]]:
|
|||||||
"required": [],
|
"required": [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "set_camera_state",
|
|
||||||
"description": (
|
|
||||||
"Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). "
|
|
||||||
"Use camera='*' to apply to all cameras at once. "
|
|
||||||
"Only call this tool when the user explicitly asks to change a camera setting. "
|
|
||||||
"Requires admin privileges."
|
|
||||||
),
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"camera": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Camera name to target, or '*' to target all cameras.",
|
|
||||||
},
|
|
||||||
"feature": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"detect",
|
|
||||||
"record",
|
|
||||||
"snapshots",
|
|
||||||
"audio",
|
|
||||||
"motion",
|
|
||||||
"enabled",
|
|
||||||
"birdseye",
|
|
||||||
"birdseye_mode",
|
|
||||||
"improve_contrast",
|
|
||||||
"ptz_autotracker",
|
|
||||||
"motion_contour_area",
|
|
||||||
"motion_threshold",
|
|
||||||
"notifications",
|
|
||||||
"audio_transcription",
|
|
||||||
"review_alerts",
|
|
||||||
"review_detections",
|
|
||||||
"object_descriptions",
|
|
||||||
"review_descriptions",
|
|
||||||
"profile",
|
|
||||||
],
|
|
||||||
"description": (
|
|
||||||
"The feature to change. Most features accept ON or OFF. "
|
|
||||||
"birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. "
|
|
||||||
"motion_contour_area and motion_threshold accept a number. "
|
|
||||||
"profile accepts a profile name or 'none' to deactivate (requires camera='*')."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["camera", "feature", "value"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -311,7 +255,6 @@ async def _execute_search_objects(
|
|||||||
description="Execute a tool function call from an LLM.",
|
description="Execute a tool function call from an LLM.",
|
||||||
)
|
)
|
||||||
async def execute_tool(
|
async def execute_tool(
|
||||||
request: Request,
|
|
||||||
body: ToolExecuteRequest = Body(...),
|
body: ToolExecuteRequest = Body(...),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
@ -329,12 +272,6 @@ async def execute_tool(
|
|||||||
if tool_name == "search_objects":
|
if tool_name == "search_objects":
|
||||||
return await _execute_search_objects(arguments, allowed_cameras)
|
return await _execute_search_objects(arguments, allowed_cameras)
|
||||||
|
|
||||||
if tool_name == "set_camera_state":
|
|
||||||
result = await _execute_set_camera_state(request, arguments)
|
|
||||||
return JSONResponse(
|
|
||||||
content=result, status_code=200 if result.get("success") else 400
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -437,46 +374,6 @@ async def _get_live_frame_image_url(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _execute_set_camera_state(
|
|
||||||
request: Request,
|
|
||||||
arguments: Dict[str, Any],
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
role = request.headers.get("remote-role", "")
|
|
||||||
if "admin" not in [r.strip() for r in role.split(",")]:
|
|
||||||
return {"error": "Admin privileges required to change camera settings."}
|
|
||||||
|
|
||||||
camera = arguments.get("camera", "").strip()
|
|
||||||
feature = arguments.get("feature", "").strip()
|
|
||||||
value = arguments.get("value", "").strip()
|
|
||||||
|
|
||||||
if not camera or not feature or not value:
|
|
||||||
return {"error": "camera, feature, and value are all required."}
|
|
||||||
|
|
||||||
dispatcher = request.app.dispatcher
|
|
||||||
frigate_config = request.app.frigate_config
|
|
||||||
|
|
||||||
if feature == "profile":
|
|
||||||
if camera != "*":
|
|
||||||
return {"error": "Profile feature requires camera='*'."}
|
|
||||||
dispatcher._receive("profile/set", value)
|
|
||||||
return {"success": True, "camera": camera, "feature": feature, "value": value}
|
|
||||||
|
|
||||||
if feature not in dispatcher._camera_settings_handlers:
|
|
||||||
return {"error": f"Unknown feature: {feature}"}
|
|
||||||
|
|
||||||
if camera == "*":
|
|
||||||
cameras = list(frigate_config.cameras.keys())
|
|
||||||
elif camera not in frigate_config.cameras:
|
|
||||||
return {"error": f"Camera '{camera}' not found."}
|
|
||||||
else:
|
|
||||||
cameras = [camera]
|
|
||||||
|
|
||||||
for cam in cameras:
|
|
||||||
dispatcher._receive(f"{cam}/{feature}/set", value)
|
|
||||||
|
|
||||||
return {"success": True, "camera": camera, "feature": feature, "value": value}
|
|
||||||
|
|
||||||
|
|
||||||
async def _execute_tool_internal(
|
async def _execute_tool_internal(
|
||||||
tool_name: str,
|
tool_name: str,
|
||||||
arguments: Dict[str, Any],
|
arguments: Dict[str, Any],
|
||||||
@ -501,8 +398,6 @@ async def _execute_tool_internal(
|
|||||||
except (json.JSONDecodeError, AttributeError) as e:
|
except (json.JSONDecodeError, AttributeError) as e:
|
||||||
logger.warning(f"Failed to extract tool result: {e}")
|
logger.warning(f"Failed to extract tool result: {e}")
|
||||||
return {"error": "Failed to parse tool result"}
|
return {"error": "Failed to parse tool result"}
|
||||||
elif tool_name == "set_camera_state":
|
|
||||||
return await _execute_set_camera_state(request, arguments)
|
|
||||||
elif tool_name == "get_live_context":
|
elif tool_name == "get_live_context":
|
||||||
camera = arguments.get("camera")
|
camera = arguments.get("camera")
|
||||||
if not camera:
|
if not camera:
|
||||||
|
|||||||
@ -30,8 +30,10 @@ class AppPutRoleBody(BaseModel):
|
|||||||
role: str
|
role: str
|
||||||
|
|
||||||
|
|
||||||
class CameraSetBody(BaseModel):
|
class ProfileSetBody(BaseModel):
|
||||||
value: str = Field(..., description="The value to set for the feature")
|
profile: Optional[str] = Field(
|
||||||
|
default=None, description="Profile name to activate, or null to deactivate"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MediaSyncBody(BaseModel):
|
class MediaSyncBody(BaseModel):
|
||||||
|
|||||||
@ -38,7 +38,6 @@ class MqttClient(Communicator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.publish("available", "stopped", retain=True)
|
|
||||||
self.client.disconnect()
|
self.client.disconnect()
|
||||||
|
|
||||||
def _set_initial_topics(self) -> None:
|
def _set_initial_topics(self) -> None:
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
|
|||||||
|
|
||||||
const handleActivateProfile = async (profileName: string | null) => {
|
const handleActivateProfile = async (profileName: string | null) => {
|
||||||
try {
|
try {
|
||||||
await axios.put("camera/*/set/profile", { value: profileName ?? "none" });
|
await axios.put("profile/set", { profile: profileName || null });
|
||||||
await updateProfiles();
|
await updateProfiles();
|
||||||
toast.success(
|
toast.success(
|
||||||
profileName
|
profileName
|
||||||
|
|||||||
@ -207,8 +207,8 @@ export default function ProfilesView({
|
|||||||
async (profile: string | null) => {
|
async (profile: string | null) => {
|
||||||
setActivating(true);
|
setActivating(true);
|
||||||
try {
|
try {
|
||||||
await axios.put("camera/*/set/profile", {
|
await axios.put("profile/set", {
|
||||||
value: profile ?? "none",
|
profile: profile || null,
|
||||||
});
|
});
|
||||||
await updateProfiles();
|
await updateProfiles();
|
||||||
toast.success(
|
toast.success(
|
||||||
@ -244,7 +244,7 @@ export default function ProfilesView({
|
|||||||
try {
|
try {
|
||||||
// If this profile is active, deactivate it first
|
// If this profile is active, deactivate it first
|
||||||
if (activeProfile === deleteProfile) {
|
if (activeProfile === deleteProfile) {
|
||||||
await axios.put("camera/*/set/profile", { value: "none" });
|
await axios.put("profile/set", { profile: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the profile from all cameras and the top-level definition
|
// Remove the profile from all cameras and the top-level definition
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user