add profile support and decouple relative move from autotracking

This commit is contained in:
Josh Hawkins 2026-03-23 12:12:52 -05:00
parent 5d67ba76fd
commit 380780b4fa
2 changed files with 182 additions and 122 deletions

View File

@ -117,6 +117,11 @@ class OnvifConfig(FrigateBaseModel):
title="Disable TLS verify", title="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: Optional[str] = Field(
default=None,
title="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: PtzAutotrackConfig = Field( autotracking: PtzAutotrackConfig = Field(
default_factory=PtzAutotrackConfig, default_factory=PtzAutotrackConfig,
title="Autotracking", title="Autotracking",

View File

@ -161,22 +161,56 @@ class OnvifController:
) )
return False return False
profile = None # build list of valid PTZ profiles
for _, onvif_profile in enumerate(profiles): valid_profiles = [
if ( p
onvif_profile.VideoEncoderConfiguration for p in profiles
and onvif_profile.PTZConfiguration if p.VideoEncoderConfiguration
and p.PTZConfiguration
and ( and (
onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace p.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None
is not None or p.PTZConfiguration.DefaultContinuousZoomVelocitySpace is not None
or onvif_profile.PTZConfiguration.DefaultContinuousZoomVelocitySpace
is not None
) )
): ]
# use the first profile that has a valid ptz configuration
profile = onvif_profile # log available profiles with names and tokens for debugging
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}") for p in valid_profiles:
logger.debug(
"Onvif profile for %s: name='%s', token='%s'",
camera_name,
getattr(p, "Name", None),
p.token,
)
configured_profile = self.config.cameras[camera_name].onvif.profile
profile = None
if configured_profile is not None:
# match by exact token first, then by name
for p in valid_profiles:
if p.token == configured_profile:
profile = p
break break
if profile is None:
for p in valid_profiles:
if getattr(p, "Name", None) == configured_profile:
profile = p
break
if profile is None:
available = [
f"name='{getattr(p, 'Name', None)}', token='{p.token}'"
for p in valid_profiles
]
logger.error(
"Onvif profile '%s' not found for camera %s. Available profiles: %s",
configured_profile,
camera_name,
available,
)
return False
else:
# use the first profile that has a valid ptz configuration
profile = valid_profiles[0] if valid_profiles else None
if profile is None: if profile is None:
logger.error( logger.error(
@ -184,6 +218,8 @@ class OnvifController:
) )
return False return False
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}")
# get the PTZ config for the profile # get the PTZ config for the profile
try: try:
configs = profile.PTZConfiguration configs = profile.PTZConfiguration
@ -218,21 +254,25 @@ class OnvifController:
move_request.ProfileToken = profile.token move_request.ProfileToken = profile.token
self.cams[camera_name]["move_request"] = move_request self.cams[camera_name]["move_request"] = move_request
# extra setup for autotracking cameras # get PTZ configuration options for feature detection and relative movement
if ( ptz_config = None
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config fov_space_id = None
and self.config.cameras[camera_name].onvif.autotracking.enabled
): try:
request = ptz.create_type("GetConfigurationOptions") request = ptz.create_type("GetConfigurationOptions")
request.ConfigurationToken = profile.PTZConfiguration.token request.ConfigurationToken = profile.PTZConfiguration.token
ptz_config = await ptz.GetConfigurationOptions(request) ptz_config = await ptz.GetConfigurationOptions(request)
logger.debug(f"Onvif config for {camera_name}: {ptz_config}") logger.debug(
f"Onvif PTZ configuration options for {camera_name}: {ptz_config}"
service_capabilities_request = ptz.create_type("GetServiceCapabilities") )
self.cams[camera_name]["service_capabilities_request"] = ( except (Fault, ONVIFError, TransportError, Exception) as e:
service_capabilities_request logger.debug(
f"Unable to get PTZ configuration options for {camera_name}: {e}"
) )
# detect FOV translation space for relative movement
if ptz_config is not None:
try:
fov_space_id = next( fov_space_id = next(
( (
i i
@ -243,23 +283,63 @@ class OnvifController:
), ),
None, None,
) )
except (AttributeError, TypeError):
fov_space_id = None
# status request for autotracking and filling ptz-parameters autotracking_config = self.config.cameras[camera_name].onvif.autotracking
autotracking_enabled = (
autotracking_config.enabled_in_config and autotracking_config.enabled
)
# autotracking-only: status request and service capabilities
if autotracking_enabled:
status_request = ptz.create_type("GetStatus") status_request = ptz.create_type("GetStatus")
status_request.ProfileToken = profile.token status_request.ProfileToken = profile.token
self.cams[camera_name]["status_request"] = status_request self.cams[camera_name]["status_request"] = status_request
try:
status = await ptz.GetStatus(status_request)
logger.debug(f"Onvif status config for {camera_name}: {status}")
except Exception as e:
logger.warning(f"Unable to get status from camera: {camera_name}: {e}")
status = None
# autotracking relative panning/tilting needs a relative zoom value set to 0 service_capabilities_request = ptz.create_type("GetServiceCapabilities")
# if camera supports relative movement self.cams[camera_name]["service_capabilities_request"] = (
service_capabilities_request
)
# setup relative move request when FOV relative movement is supported
if ( if (
self.config.cameras[camera_name].onvif.autotracking.zooming fov_space_id is not None
!= ZoomingModeEnum.disabled and configs.DefaultRelativePanTiltTranslationSpace is not None
):
# one-off GetStatus to seed Translation field
status = None
try:
one_off_status_request = ptz.create_type("GetStatus")
one_off_status_request.ProfileToken = profile.token
status = await ptz.GetStatus(one_off_status_request)
logger.debug(f"Onvif status for {camera_name}: {status}")
except Exception as e:
logger.warning(f"Unable to get status from camera {camera_name}: {e}")
rel_move_request = ptz.create_type("RelativeMove")
rel_move_request.ProfileToken = profile.token
logger.debug(f"{camera_name}: Relative move request: {rel_move_request}")
fov_uri = ptz_config["Spaces"]["RelativePanTiltTranslationSpace"][
fov_space_id
]["URI"]
if rel_move_request.Translation is None:
if status is not None:
# seed from current position
rel_move_request.Translation = status.Position
rel_move_request.Translation.PanTilt.space = fov_uri
else:
# fallback: construct Translation explicitly
rel_move_request.Translation = {
"PanTilt": {"x": 0, "y": 0, "space": fov_uri}
}
# configure zoom on relative move request
if (
autotracking_enabled
and autotracking_config.zooming != ZoomingModeEnum.disabled
): ):
zoom_space_id = next( zoom_space_id = next(
( (
@ -271,60 +351,43 @@ class OnvifController:
), ),
None, None,
) )
# setup relative moving request for autotracking
move_request = ptz.create_type("RelativeMove")
move_request.ProfileToken = profile.token
logger.debug(f"{camera_name}: Relative move request: {move_request}")
if move_request.Translation is None and fov_space_id is not None:
move_request.Translation = status.Position
move_request.Translation.PanTilt.space = ptz_config["Spaces"][
"RelativePanTiltTranslationSpace"
][fov_space_id]["URI"]
# try setting relative zoom translation space
try: try:
if (
self.config.cameras[camera_name].onvif.autotracking.zooming
!= ZoomingModeEnum.disabled
):
if zoom_space_id is not None: if zoom_space_id is not None:
move_request.Translation.Zoom.space = ptz_config["Spaces"][ rel_move_request.Translation.Zoom.space = ptz_config["Spaces"][
"RelativeZoomTranslationSpace" "RelativeZoomTranslationSpace"
][zoom_space_id]["URI"] ][zoom_space_id]["URI"]
else:
if (
move_request["Translation"] is not None
and "Zoom" in move_request["Translation"]
):
del move_request["Translation"]["Zoom"]
if (
move_request["Speed"] is not None
and "Zoom" in move_request["Speed"]
):
del move_request["Speed"]["Zoom"]
logger.debug(
f"{camera_name}: Relative move request after deleting zoom: {move_request}"
)
except Exception as e: except Exception as e:
self.config.cameras[ autotracking_config.zooming = ZoomingModeEnum.disabled
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( logger.warning(
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
) )
else:
if move_request.Speed is None: # remove zoom fields from relative move request
move_request.Speed = configs.DefaultPTZSpeed if configs else None if (
rel_move_request["Translation"] is not None
and "Zoom" in rel_move_request["Translation"]
):
del rel_move_request["Translation"]["Zoom"]
if (
rel_move_request["Speed"] is not None
and "Zoom" in rel_move_request["Speed"]
):
del rel_move_request["Speed"]["Zoom"]
logger.debug( logger.debug(
f"{camera_name}: Relative move request after setup: {move_request}" f"{camera_name}: Relative move request after deleting zoom: {rel_move_request}"
) )
self.cams[camera_name]["relative_move_request"] = move_request
# setup absolute moving request for autotracking zooming if rel_move_request.Speed is None:
move_request = ptz.create_type("AbsoluteMove") rel_move_request.Speed = configs.DefaultPTZSpeed if configs else None
move_request.ProfileToken = profile.token logger.debug(
self.cams[camera_name]["absolute_move_request"] = move_request f"{camera_name}: Relative move request after setup: {rel_move_request}"
)
self.cams[camera_name]["relative_move_request"] = rel_move_request
# setup absolute move request
abs_move_request = ptz.create_type("AbsoluteMove")
abs_move_request.ProfileToken = profile.token
self.cams[camera_name]["absolute_move_request"] = abs_move_request
# setup existing presets # setup existing presets
try: try:
@ -358,48 +421,48 @@ class OnvifController:
if configs.DefaultRelativeZoomTranslationSpace: if configs.DefaultRelativeZoomTranslationSpace:
supported_features.append("zoom-r") supported_features.append("zoom-r")
if ( if ptz_config is not None:
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
and self.config.cameras[camera_name].onvif.autotracking.enabled
):
try: try:
# get camera's zoom limits from onvif config
self.cams[camera_name]["relative_zoom_range"] = ( self.cams[camera_name]["relative_zoom_range"] = (
ptz_config.Spaces.RelativeZoomTranslationSpace[0] ptz_config.Spaces.RelativeZoomTranslationSpace[0]
) )
except Exception as e: except Exception as e:
if ( if autotracking_config.zooming == ZoomingModeEnum.relative:
self.config.cameras[camera_name].onvif.autotracking.zooming autotracking_config.zooming = ZoomingModeEnum.disabled
== ZoomingModeEnum.relative
):
self.config.cameras[
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( logger.warning(
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}" f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
) )
if configs.DefaultAbsoluteZoomPositionSpace: if configs.DefaultAbsoluteZoomPositionSpace:
supported_features.append("zoom-a") supported_features.append("zoom-a")
if ( if ptz_config is not None:
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
and self.config.cameras[camera_name].onvif.autotracking.enabled
):
try: try:
# get camera's zoom limits from onvif config
self.cams[camera_name]["absolute_zoom_range"] = ( self.cams[camera_name]["absolute_zoom_range"] = (
ptz_config.Spaces.AbsoluteZoomPositionSpace[0] ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
) )
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
except Exception as e: except Exception as e:
if self.config.cameras[camera_name].onvif.autotracking.zooming: if autotracking_config.zooming != ZoomingModeEnum.disabled:
self.config.cameras[ autotracking_config.zooming = ZoomingModeEnum.disabled
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning( logger.warning(
f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}" f"Disabling autotracking zooming for {camera_name}: Absolute zoom not supported. Exception: {e}"
) )
# disable autotracking zoom if required ranges are unavailable
if autotracking_config.zooming != ZoomingModeEnum.disabled:
if autotracking_config.zooming == ZoomingModeEnum.relative:
if "relative_zoom_range" not in self.cams[camera_name]:
autotracking_config.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Disabling autotracking zooming for {camera_name}: Relative zoom range unavailable"
)
if autotracking_config.zooming == ZoomingModeEnum.absolute:
if "absolute_zoom_range" not in self.cams[camera_name]:
autotracking_config.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Disabling autotracking zooming for {camera_name}: Absolute zoom range unavailable"
)
if ( if (
self.cams[camera_name]["video_source_token"] is not None self.cams[camera_name]["video_source_token"] is not None
and imaging is not None and imaging is not None
@ -416,10 +479,9 @@ class OnvifController:
except (Fault, ONVIFError, TransportError, Exception) as e: except (Fault, ONVIFError, TransportError, Exception) as e:
logger.debug(f"Focus not supported for {camera_name}: {e}") logger.debug(f"Focus not supported for {camera_name}: {e}")
# detect FOV relative movement support
if ( if (
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config fov_space_id is not None
and self.config.cameras[camera_name].onvif.autotracking.enabled
and fov_space_id is not None
and configs.DefaultRelativePanTiltTranslationSpace is not None and configs.DefaultRelativePanTiltTranslationSpace is not None
): ):
supported_features.append("pt-r-fov") supported_features.append("pt-r-fov")
@ -548,11 +610,8 @@ class OnvifController:
move_request.Translation.PanTilt.x = pan move_request.Translation.PanTilt.x = pan
move_request.Translation.PanTilt.y = tilt move_request.Translation.PanTilt.y = tilt
if ( # include zoom if requested and camera supports relative zoom
"zoom-r" in self.cams[camera_name]["features"] if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
and self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative
):
move_request.Speed = { move_request.Speed = {
"PanTilt": { "PanTilt": {
"x": speed, "x": speed,
@ -568,11 +627,7 @@ class OnvifController:
move_request.Translation.PanTilt.x = 0 move_request.Translation.PanTilt.x = 0
move_request.Translation.PanTilt.y = 0 move_request.Translation.PanTilt.y = 0
if ( if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
"zoom-r" in self.cams[camera_name]["features"]
and self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative
):
move_request.Translation.Zoom.x = 0 move_request.Translation.Zoom.x = 0
self.cams[camera_name]["active"] = False self.cams[camera_name]["active"] = False