Skip to content

Instantly share code, notes, and snippets.

@hawkeye217
Last active March 20, 2025 18:44
Show Gist options
  • Save hawkeye217/152a1d4ba80760dac95d46e143d37112 to your computer and use it in GitHub Desktop.
Save hawkeye217/152a1d4ba80760dac95d46e143d37112 to your computer and use it in GitHub Desktop.
Check if an ONVIF-capable IP PTZ camera supports RelativeMove with FOV
# This script can help you determine if your PTZ is capable of
# working with Frigate NVR's autotracker.
#
# Cameras with a "YES" printed for each parameter at the end of
# the output will likely be supported by Frigate.
#
# Make sure you're using python3 with the onvif-zeep package
# Update the values for your camera below, then run:
# pip3 install onvif-zeep
# python3 ./fovtest.py
from onvif import ONVIFCamera
# UPDATE the IP address, ONVIF port, "admin" and "password" with your camera's details.
mycam = ONVIFCamera('192.168.1.100', 80, 'admin', 'password', '/etc/onvif/wsdl/')
print('Connected to ONVIF camera')
# Create media service object
media = mycam.create_media_service()
print('Created media service object')
print
# Get target profile
media_profiles = media.GetProfiles()
print('Media profiles')
print(media_profiles)
for key, onvif_profile in enumerate(media_profiles):
if (
not onvif_profile.VideoEncoderConfiguration
or onvif_profile.VideoEncoderConfiguration.Encoding != "H264"
):
continue
# Configure PTZ options
if onvif_profile.PTZConfiguration:
if onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None:
media_profile = onvif_profile
token = media_profile.token
print('Chosen token')
print(token)
print
# Create ptz service object
print('Creating PTZ object')
ptz = mycam.create_ptz_service()
print('Created PTZ service object')
print
# Get PTZ configuration options for getting option ranges
request = ptz.create_type("GetConfigurations")
configs = ptz.GetConfigurations(request)[0]
print('PTZ configurations:')
print(configs)
print()
request = ptz.create_type('GetConfigurationOptions')
request.ConfigurationToken = media_profile.PTZConfiguration.token
ptz_configuration_options = ptz.GetConfigurationOptions(request)
print('PTZ configuration options:')
print(ptz_configuration_options)
print()
print('PTZ service capabilities:')
request = ptz.create_type('GetServiceCapabilities')
service_capabilities = ptz.GetServiceCapabilities(request)
print(service_capabilities)
print()
print('PTZ status:')
request = ptz.create_type("GetStatus")
request.ProfileToken = token
status = ptz.GetStatus(request)
print(status)
pantilt_space_id = next(
(
i
for i, space in enumerate(
ptz_configuration_options.Spaces.RelativePanTiltTranslationSpace
)
if "TranslationSpaceFov" in space["URI"]
),
None,
)
zoom_space_id = next(
(
i
for i, space in enumerate(
ptz_configuration_options.Spaces.RelativeZoomTranslationSpace
)
if "TranslationGenericSpace" in space["URI"]
),
None,
)
def find_by_key(dictionary, target_key):
if target_key in dictionary:
return dictionary[target_key]
else:
for value in dictionary.values():
if isinstance(value, dict):
result = find_by_key(value, target_key)
if result is not None:
return result
return None
if find_by_key(vars(service_capabilities), "MoveStatus"):
print("YES - GetServiceCapabilities shows that the camera supports MoveStatus.")
else:
print("NO - GetServiceCapabilities shows that the camera does not support MoveStatus.")
# there doesn't seem to be an onvif standard with this optional parameter
# some cameras can report MoveStatus with or without PanTilt or Zoom attributes
pan_tilt_status = getattr(status.MoveStatus, "PanTilt", None)
zoom_status = getattr(status.MoveStatus, "Zoom", None)
if pan_tilt_status is not None and pan_tilt_status == "IDLE" and (
zoom_status is None or zoom_status == "IDLE"
):
print("YES - MoveStatus is reporting IDLE.")
# if it's not an attribute, see if MoveStatus even exists in the status result
if pan_tilt_status is None:
pan_tilt_status = getattr(status.MoveStatus, "MoveStatus", None)
# we're unsupported
if pan_tilt_status is None or (isinstance(pan_tilt_status, str) and pan_tilt_status not in [
"IDLE",
"MOVING",
]):
print("NO - MoveStatus not reporting IDLE or MOVING.")
if pantilt_space_id is not None and configs.DefaultRelativePanTiltTranslationSpace is not None:
print("YES - RelativeMove Pan/Tilt (FOV) is supported.")
else:
print("NO - RelativeMove Pan/Tilt is unsupported.")
if zoom_space_id is not None:
print("YES - RelativeMove Zoom is supported.")
else:
print("NO - RelativeMove Zoom is unsupported.")
@VeganBiker
Copy link

I'm getting the following for all my cameras:
Connected to ONVIF camera
Created media service object
Media profiles
[{
'Name': 'MainStream',
'VideoSourceConfiguration': {
'Name': 'VideoSourceMain',
'UseCount': 2,
'SourceToken': 'VideoSourceMain',
'Bounds': {
'x': 0,
'y': 0,
'width': 3840,
'height': 2160
},
'_value_1': None,
'Extension': None,
'token': 'VideoSourceMain',
'_attr_1': {
}
},
'AudioSourceConfiguration': {
'Name': 'AudioMainName',
'UseCount': 2,
'SourceToken': 'AudioMainSrcToken',
'_value_1': None,
'token': 'AudioMainToken',
'_attr_1': {
}
},
'VideoEncoderConfiguration': {
'Name': 'VideoEncodeMain',
'UseCount': 1,
'Encoding': 'H264',
'Resolution': {
'Width': 3840,
'Height': 2160
},
'Quality': 100.0,
'RateControl': {
'FrameRateLimit': 20,
'EncodingInterval': 1,
'BitrateLimit': 0
},
'MPEG4': {
'GovLength': 0,
'Mpeg4Profile': 'SP'
},
'H264': {
'GovLength': 20,
'H264Profile': 'High'
},
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '192.168.10.61',
'IPv6Address': None
},
'Port': 0,
'TTL': 0,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(seconds=720),
'_value_1': None,
'token': 'VideoEncodeMain',
'_attr_1': {
}
},
'AudioEncoderConfiguration': {
'Name': 'AudioMain',
'UseCount': 2,
'Encoding': 'G711',
'Bitrate': 64000,
'SampleRate': 16000,
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '192.168.10.61',
'IPv6Address': None
},
'Port': 80,
'TTL': 1,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(microseconds=60000),
'_value_1': None,
'token': 'G711',
'_attr_1': {
}
},
'VideoAnalyticsConfiguration': {
'Name': 'VideoAnalyticsName',
'UseCount': 3,
'AnalyticsEngineConfiguration': {
'AnalyticsModule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'Sensitivity',
'Value': '80'
}
],
'ElementItem': [
{
'_value_1': <Element {http://www.onvif.org/ver10/schema}CellLayout at 0x7f02b19844c0>,
'Name': 'Layout'
}
],
'Extension': None,
'_attr_1': None
},
'Name': 'MyCellMotionModule',
'Type': 'tt:CellMotionEngine'
}
],
'Extension': None,
'_attr_1': None
},
'RuleEngineConfiguration': {
'Rule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'MinCount',
'Value': '5'
},
{
'Name': 'AlarmOnDelay',
'Value': '100'
},
{
'Name': 'AlarmOffDelay',
'Value': '100'
},
{
'Name': 'ActiveCells',
'Value': '0P8A8A=='
}
],
'ElementItem': [],
'Extension': None,
'_attr_1': None
},
'Name': 'MyMotionDetectorRule',
'Type': 'tt:CellMotionDetector'
}
],
'Extension': None,
'_attr_1': None
},
'_value_1': None,
'token': 'VideoAnalyticsToken',
'_attr_1': {
}
},
'PTZConfiguration': {
'Name': 'ptz0',
'UseCount': 2,
'NodeToken': 'ptz0',
'DefaultAbsolutePantTiltPositionSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'DefaultAbsoluteZoomPositionSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'DefaultRelativePanTiltTranslationSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace',
'DefaultRelativeZoomTranslationSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace',
'DefaultContinuousPanTiltVelocitySpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace',
'DefaultContinuousZoomVelocitySpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace',
'DefaultPTZSpeed': {
'PanTilt': {
'x': 1.0,
'y': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace'
},
'Zoom': {
'x': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace'
}
},
'DefaultPTZTimeout': datetime.timedelta(seconds=60),
'PanTiltLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'ZoomLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'Extension': None,
'token': 'ptz0',
'_attr_1': {
}
},
'MetadataConfiguration': None,
'Extension': None,
'token': 'MainStream',
'fixed': False,
'_attr_1': {
}
}, {
'Name': 'SubStream',
'VideoSourceConfiguration': {
'Name': 'VideoSourceMain',
'UseCount': 2,
'SourceToken': 'VideoSourceMain',
'Bounds': {
'x': 0,
'y': 0,
'width': 720,
'height': 480
},
'_value_1': None,
'Extension': None,
'token': 'VideoSourceMain',
'_attr_1': {
}
},
'AudioSourceConfiguration': {
'Name': 'AudioMainName',
'UseCount': 2,
'SourceToken': 'AudioMainSrcToken',
'_value_1': None,
'token': 'AudioMainToken',
'_attr_1': {
}
},
'VideoEncoderConfiguration': {
'Name': 'VideoEncodeSub',
'UseCount': 1,
'Encoding': 'H264',
'Resolution': {
'Width': 720,
'Height': 480
},
'Quality': 100.0,
'RateControl': {
'FrameRateLimit': 25,
'EncodingInterval': 1,
'BitrateLimit': 2048
},
'MPEG4': {
'GovLength': 0,
'Mpeg4Profile': 'SP'
},
'H264': {
'GovLength': 20,
'H264Profile': 'High'
},
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '192.168.10.61',
'IPv6Address': None
},
'Port': 0,
'TTL': 0,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(seconds=720),
'_value_1': None,
'token': 'VideoEncodeSub',
'_attr_1': {
}
},
'AudioEncoderConfiguration': {
'Name': 'AudioMain',
'UseCount': 2,
'Encoding': 'G711',
'Bitrate': 64000,
'SampleRate': 16000,
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '192.168.10.61',
'IPv6Address': None
},
'Port': 80,
'TTL': 1,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(microseconds=60000),
'_value_1': None,
'token': 'G711',
'_attr_1': {
}
},
'VideoAnalyticsConfiguration': {
'Name': 'VideoAnalyticsName',
'UseCount': 3,
'AnalyticsEngineConfiguration': {
'AnalyticsModule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'Sensitivity',
'Value': '80'
}
],
'ElementItem': [
{
'_value_1': <Element {http://www.onvif.org/ver10/schema}CellLayout at 0x7f02b1989c40>,
'Name': 'Layout'
}
],
'Extension': None,
'_attr_1': None
},
'Name': 'MyCellMotionModule',
'Type': 'tt:CellMotionEngine'
}
],
'Extension': None,
'_attr_1': None
},
'RuleEngineConfiguration': {
'Rule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'MinCount',
'Value': '5'
},
{
'Name': 'AlarmOnDelay',
'Value': '100'
},
{
'Name': 'AlarmOffDelay',
'Value': '100'
},
{
'Name': 'ActiveCells',
'Value': '0P8A8A=='
}
],
'ElementItem': [],
'Extension': None,
'_attr_1': None
},
'Name': 'MyMotionDetectorRule',
'Type': 'tt:CellMotionDetector'
}
],
'Extension': None,
'_attr_1': None
},
'_value_1': None,
'token': 'VideoAnalyticsToken',
'_attr_1': {
}
},
'PTZConfiguration': {
'Name': 'ptz0',
'UseCount': 2,
'NodeToken': 'ptz0',
'DefaultAbsolutePantTiltPositionSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'DefaultAbsoluteZoomPositionSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'DefaultRelativePanTiltTranslationSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace',
'DefaultRelativeZoomTranslationSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace',
'DefaultContinuousPanTiltVelocitySpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace',
'DefaultContinuousZoomVelocitySpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace',
'DefaultPTZSpeed': {
'PanTilt': {
'x': 1.0,
'y': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace'
},
'Zoom': {
'x': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace'
}
},
'DefaultPTZTimeout': datetime.timedelta(seconds=60),
'PanTiltLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'ZoomLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'Extension': None,
'token': 'ptz0',
'_attr_1': {
}
},
'MetadataConfiguration': None,
'Extension': None,
'token': 'SubStream',
'fixed': False,
'_attr_1': {
}
}]
Chosen token
SubStream
Creating PTZ object
Created PTZ service object
PTZ configurations:
{
'Name': 'ptz0',
'UseCount': 2,
'NodeToken': 'ptz0',
'DefaultAbsolutePantTiltPositionSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'DefaultAbsoluteZoomPositionSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'DefaultRelativePanTiltTranslationSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace',
'DefaultRelativeZoomTranslationSpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace',
'DefaultContinuousPanTiltVelocitySpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace',
'DefaultContinuousZoomVelocitySpace': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace',
'DefaultPTZSpeed': {
'PanTilt': {
'x': 1.0,
'y': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace'
},
'Zoom': {
'x': 1.0,
'space': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace'
}
},
'DefaultPTZTimeout': datetime.timedelta(seconds=60),
'PanTiltLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'ZoomLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'Extension': None,
'token': 'ptz0',
'_attr_1': {
}
}

PTZ configuration options:
{
'Spaces': {
'AbsolutePanTiltPositionSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'AbsoluteZoomPositionSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'RelativePanTiltTranslationSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'RelativeZoomTranslationSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'ContinuousPanTiltVelocitySpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'ContinuousZoomVelocitySpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'PanTiltSpeedSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'ZoomSpeedSpace': [
{
'URI': 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
}
}
],
'Extension': None,
'_attr_1': None
},
'PTZTimeout': {
'Min': datetime.timedelta(seconds=1),
'Max': datetime.timedelta(seconds=60)
},
'_value_1': None,
'PTControlDirection': None,
'Extension': None,
'_attr_1': None
}

PTZ service capabilities:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/dist-packages/onvif/client.py", line 23, in wrapped
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/onvif/client.py", line 153, in wrapped
return call(params, callback)
^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/onvif/client.py", line 140, in call
ret = func(**params)
^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/zeep/proxy.py", line 46, in call
return self._proxy._binding.send(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/zeep/wsdl/bindings/soap.py", line 135, in send
return self.process_reply(client, operation_obj, response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/zeep/wsdl/bindings/soap.py", line 229, in process_reply
return self.process_error(doc, operation)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/zeep/wsdl/bindings/soap.py", line 391, in process_error
raise Fault(
zeep.exceptions.Fault

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/home/sdansro/./fovtest.py", line 65, in
service_capabilities = ptz.GetServiceCapabilities(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/onvif/client.py", line 26, in wrapped
raise ONVIFError(err)
onvif.exceptions.ONVIFError: Unknown error:

@hawkeye217
Copy link
Author

Looks like your camera's ONVIF implementation doesn't support a the GetServiceCapabilities call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment