add profile selection to UI

This commit is contained in:
Josh Hawkins 2026-03-23 13:24:33 -05:00
parent 5703a8d790
commit 6215501ae8
9 changed files with 123 additions and 10 deletions

View File

@ -23,6 +23,7 @@ class CameraConfigUpdateEnum(str, Enum):
notifications = "notifications"
objects = "objects"
object_genai = "object_genai"
onvif = "onvif"
record = "record"
remove = "remove" # for removing a camera
review = "review"

View File

@ -118,6 +118,7 @@ class OnvifController:
"active": False,
"features": [],
"presets": {},
"profiles": [],
}
return True
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:
logger.debug(
"Onvif profile for %s: name='%s', token='%s'",
@ -838,6 +843,7 @@ class OnvifController:
"name": camera_name,
"features": self.cams[camera_name]["features"],
"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:

View File

@ -787,6 +787,10 @@
"label": "Disable TLS verify",
"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": {
"label": "Autotracking",
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",

View File

@ -1536,6 +1536,10 @@
"label": "Disable TLS verify",
"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": {
"label": "Autotracking",
"description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.",

View File

@ -1571,5 +1571,9 @@
"hardwareNone": "No hardware acceleration",
"hardwareAuto": "Automatic hardware acceleration"
}
},
"onvif": {
"profileAuto": "Auto",
"profileLoading": "Loading profiles..."
}
}

View File

@ -3,20 +3,12 @@ import type { SectionConfigOverrides } from "./types";
const onvif: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/cameras#setting-up-camera-ptz-controls",
restartRequired: [
"host",
"port",
"user",
"password",
"tls_insecure",
"ignore_time_mismatch",
"autotracking.calibrate_on_startup",
],
fieldOrder: [
"host",
"port",
"user",
"password",
"profile",
"tls_insecure",
"ignore_time_mismatch",
"autotracking",
@ -27,10 +19,23 @@ const onvif: SectionConfigOverrides = {
],
advancedFields: ["tls_insecure", "ignore_time_mismatch"],
overrideFields: [],
restartRequired: [
"host",
"port",
"user",
"password",
"profile",
"tls_insecure",
"ignore_time_mismatch",
"autotracking.calibrate_on_startup",
],
uiSchema: {
host: {
"ui:options": { size: "sm" },
},
profile: {
"ui:widget": "onvifProfile",
},
autotracking: {
required_zones: {
"ui:widget": "zoneNames",

View File

@ -29,6 +29,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
import { CameraPathWidget } from "./widgets/CameraPathWidget";
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
import { FieldTemplate } from "./templates/FieldTemplate";
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
@ -79,6 +80,7 @@ export const frigateTheme: FrigateTheme = {
timezoneSelect: TimezoneSelectWidget,
optionalField: OptionalFieldWidget,
semanticSearchModel: SemanticSearchModelWidget,
onvifProfile: OnvifProfileWidget,
},
templates: {
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,

View File

@ -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>
);
}

View File

@ -7,8 +7,14 @@ type PtzFeature =
| "pt-r-fov"
| "focus";
export type OnvifProfile = {
name: string;
token: string;
};
export type CameraPtzInfo = {
name: string;
features: PtzFeature[];
presets: string[];
profiles: OnvifProfile[];
};