mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-06 15:17:37 +03:00
add profile selection to UI
This commit is contained in:
parent
5703a8d790
commit
6215501ae8
@ -23,6 +23,7 @@ class CameraConfigUpdateEnum(str, Enum):
|
|||||||
notifications = "notifications"
|
notifications = "notifications"
|
||||||
objects = "objects"
|
objects = "objects"
|
||||||
object_genai = "object_genai"
|
object_genai = "object_genai"
|
||||||
|
onvif = "onvif"
|
||||||
record = "record"
|
record = "record"
|
||||||
remove = "remove" # for removing a camera
|
remove = "remove" # for removing a camera
|
||||||
review = "review"
|
review = "review"
|
||||||
|
|||||||
@ -118,6 +118,7 @@ class OnvifController:
|
|||||||
"active": False,
|
"active": False,
|
||||||
"features": [],
|
"features": [],
|
||||||
"presets": {},
|
"presets": {},
|
||||||
|
"profiles": [],
|
||||||
}
|
}
|
||||||
return True
|
return True
|
||||||
except (Fault, ONVIFError, TransportError, Exception) as e:
|
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||||
@ -173,7 +174,11 @@ class OnvifController:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# log available profiles with names and tokens for debugging
|
# store available profiles for API response and log for debugging
|
||||||
|
self.cams[camera_name]["profiles"] = [
|
||||||
|
{"name": getattr(p, "Name", None) or p.token, "token": p.token}
|
||||||
|
for p in valid_profiles
|
||||||
|
]
|
||||||
for p in valid_profiles:
|
for p in valid_profiles:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Onvif profile for %s: name='%s', token='%s'",
|
"Onvif profile for %s: name='%s', token='%s'",
|
||||||
@ -838,6 +843,7 @@ class OnvifController:
|
|||||||
"name": camera_name,
|
"name": camera_name,
|
||||||
"features": self.cams[camera_name]["features"],
|
"features": self.cams[camera_name]["features"],
|
||||||
"presets": list(self.cams[camera_name]["presets"].keys()),
|
"presets": list(self.cams[camera_name]["presets"].keys()),
|
||||||
|
"profiles": self.cams[camera_name].get("profiles", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
if camera_name not in self.cams.keys() and camera_name in self.config.cameras:
|
if camera_name not in self.cams.keys() and camera_name in self.config.cameras:
|
||||||
|
|||||||
@ -787,6 +787,10 @@
|
|||||||
"label": "Disable TLS verify",
|
"label": "Disable TLS verify",
|
||||||
"description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)."
|
"description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)."
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"label": "ONVIF profile",
|
||||||
|
"description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically."
|
||||||
|
},
|
||||||
"autotracking": {
|
"autotracking": {
|
||||||
"label": "Autotracking",
|
"label": "Autotracking",
|
||||||
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",
|
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",
|
||||||
|
|||||||
@ -1536,6 +1536,10 @@
|
|||||||
"label": "Disable TLS verify",
|
"label": "Disable TLS verify",
|
||||||
"description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)."
|
"description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only)."
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"label": "ONVIF profile",
|
||||||
|
"description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically."
|
||||||
|
},
|
||||||
"autotracking": {
|
"autotracking": {
|
||||||
"label": "Autotracking",
|
"label": "Autotracking",
|
||||||
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",
|
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",
|
||||||
|
|||||||
@ -1571,5 +1571,9 @@
|
|||||||
"hardwareNone": "No hardware acceleration",
|
"hardwareNone": "No hardware acceleration",
|
||||||
"hardwareAuto": "Automatic hardware acceleration"
|
"hardwareAuto": "Automatic hardware acceleration"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"onvif": {
|
||||||
|
"profileAuto": "Auto",
|
||||||
|
"profileLoading": "Loading profiles..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,20 +3,12 @@ import type { SectionConfigOverrides } from "./types";
|
|||||||
const onvif: SectionConfigOverrides = {
|
const onvif: SectionConfigOverrides = {
|
||||||
base: {
|
base: {
|
||||||
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
|
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
|
||||||
restartRequired: [
|
|
||||||
"host",
|
|
||||||
"port",
|
|
||||||
"user",
|
|
||||||
"password",
|
|
||||||
"tls_insecure",
|
|
||||||
"ignore_time_mismatch",
|
|
||||||
"autotracking.calibrate_on_startup",
|
|
||||||
],
|
|
||||||
fieldOrder: [
|
fieldOrder: [
|
||||||
"host",
|
"host",
|
||||||
"port",
|
"port",
|
||||||
"user",
|
"user",
|
||||||
"password",
|
"password",
|
||||||
|
"profile",
|
||||||
"tls_insecure",
|
"tls_insecure",
|
||||||
"ignore_time_mismatch",
|
"ignore_time_mismatch",
|
||||||
"autotracking",
|
"autotracking",
|
||||||
@ -27,10 +19,23 @@ const onvif: SectionConfigOverrides = {
|
|||||||
],
|
],
|
||||||
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
|
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
|
||||||
overrideFields: [],
|
overrideFields: [],
|
||||||
|
restartRequired: [
|
||||||
|
"host",
|
||||||
|
"port",
|
||||||
|
"user",
|
||||||
|
"password",
|
||||||
|
"profile",
|
||||||
|
"tls_insecure",
|
||||||
|
"ignore_time_mismatch",
|
||||||
|
"autotracking.calibrate_on_startup",
|
||||||
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
host: {
|
host: {
|
||||||
"ui:options": { size: "sm" },
|
"ui:options": { size: "sm" },
|
||||||
},
|
},
|
||||||
|
profile: {
|
||||||
|
"ui:widget": "onvifProfile",
|
||||||
|
},
|
||||||
autotracking: {
|
autotracking: {
|
||||||
required_zones: {
|
required_zones: {
|
||||||
"ui:widget": "zoneNames",
|
"ui:widget": "zoneNames",
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
|
|||||||
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
||||||
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
||||||
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
||||||
|
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
||||||
|
|
||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
||||||
@ -79,6 +80,7 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
timezoneSelect: TimezoneSelectWidget,
|
timezoneSelect: TimezoneSelectWidget,
|
||||||
optionalField: OptionalFieldWidget,
|
optionalField: OptionalFieldWidget,
|
||||||
semanticSearchModel: SemanticSearchModelWidget,
|
semanticSearchModel: SemanticSearchModelWidget,
|
||||||
|
onvifProfile: OnvifProfileWidget,
|
||||||
},
|
},
|
||||||
templates: {
|
templates: {
|
||||||
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import type { ConfigFormContext } from "@/types/configForm";
|
||||||
|
import type { CameraPtzInfo } from "@/types/ptz";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const AUTO_VALUE = "__auto__";
|
||||||
|
|
||||||
|
export function OnvifProfileWidget(props: WidgetProps) {
|
||||||
|
const { id, value, disabled, readonly, onChange, schema, options } = props;
|
||||||
|
const { t } = useTranslation(["views/settings"]);
|
||||||
|
|
||||||
|
const formContext = props.registry?.formContext as
|
||||||
|
| ConfigFormContext
|
||||||
|
| undefined;
|
||||||
|
const cameraName = formContext?.cameraName;
|
||||||
|
const isCameraLevel = formContext?.level === "camera";
|
||||||
|
|
||||||
|
const { data: ptzInfo } = useSWR<CameraPtzInfo>(
|
||||||
|
isCameraLevel && cameraName ? `${cameraName}/ptz/info` : null,
|
||||||
|
{
|
||||||
|
// ONVIF may not be initialized yet when the settings page loads,
|
||||||
|
// so retry until profiles become available
|
||||||
|
refreshInterval: (data) =>
|
||||||
|
data?.profiles && data.profiles.length > 0 ? 0 : 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const profiles = ptzInfo?.profiles ?? [];
|
||||||
|
const fieldClassName = getSizedFieldClassName(options, "sm");
|
||||||
|
const hasProfiles = profiles.length > 0;
|
||||||
|
const waiting = isCameraLevel && !!cameraName && !hasProfiles;
|
||||||
|
|
||||||
|
const selected = value ?? AUTO_VALUE;
|
||||||
|
|
||||||
|
if (waiting) {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-2", fieldClassName)}>
|
||||||
|
<ActivityIndicator className="size-4" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t("onvif.profileLoading")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={String(selected)}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
onChange(val === AUTO_VALUE ? null : val);
|
||||||
|
}}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={id} className={fieldClassName}>
|
||||||
|
<SelectValue placeholder={schema.title || "Select..."} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={AUTO_VALUE}>{t("onvif.profileAuto")}</SelectItem>
|
||||||
|
{profiles.map((p) => (
|
||||||
|
<SelectItem key={p.token} value={p.token}>
|
||||||
|
{p.name !== p.token ? `${p.name} (${p.token})` : p.token}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
{!hasProfiles && value && value !== AUTO_VALUE && (
|
||||||
|
<SelectItem value={String(value)}>{String(value)}</SelectItem>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -7,8 +7,14 @@ type PtzFeature =
|
|||||||
| "pt-r-fov"
|
| "pt-r-fov"
|
||||||
| "focus";
|
| "focus";
|
||||||
|
|
||||||
|
export type OnvifProfile = {
|
||||||
|
name: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CameraPtzInfo = {
|
export type CameraPtzInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
features: PtzFeature[];
|
features: PtzFeature[];
|
||||||
presets: string[];
|
presets: string[];
|
||||||
|
profiles: OnvifProfile[];
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user