mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-07 03:11:15 +03:00
ONVIF refactor (#22629)
* add profile support and decouple relative move from autotracking * add drag to zoom * docs * add profile selection to UI * dynamically update onvif config * ui tweak * docs * docs tweak
This commit is contained in:
@@ -117,6 +117,11 @@ class OnvifConfig(FrigateBaseModel):
|
||||
title="Disable TLS verify",
|
||||
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(
|
||||
default_factory=PtzAutotrackConfig,
|
||||
title="Autotracking",
|
||||
|
||||
@@ -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"
|
||||
@@ -130,6 +131,8 @@ class CameraConfigUpdateSubscriber:
|
||||
config.lpr = updated_config
|
||||
elif update_type == CameraConfigUpdateEnum.snapshots:
|
||||
config.snapshots = updated_config
|
||||
elif update_type == CameraConfigUpdateEnum.onvif:
|
||||
config.onvif = updated_config
|
||||
elif update_type == CameraConfigUpdateEnum.zones:
|
||||
config.zones = updated_config
|
||||
|
||||
|
||||
+255
-126
@@ -15,6 +15,10 @@ from zeep.exceptions import Fault, TransportError
|
||||
|
||||
from frigate.camera import PTZMetrics
|
||||
from frigate.config import FrigateConfig, ZoomingModeEnum
|
||||
from frigate.config.camera.updater import (
|
||||
CameraConfigUpdateEnum,
|
||||
CameraConfigUpdateSubscriber,
|
||||
)
|
||||
from frigate.util.builtin import find_by_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,7 +69,14 @@ class OnvifController:
|
||||
self.camera_configs[cam_name] = cam
|
||||
self.status_locks[cam_name] = asyncio.Lock()
|
||||
|
||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||
self.config,
|
||||
self.config.cameras,
|
||||
[CameraConfigUpdateEnum.onvif],
|
||||
)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop)
|
||||
asyncio.run_coroutine_threadsafe(self._poll_config_updates(), self.loop)
|
||||
|
||||
def _run_event_loop(self) -> None:
|
||||
"""Run the event loop in a separate thread."""
|
||||
@@ -80,6 +91,52 @@ class OnvifController:
|
||||
for cam_name in self.camera_configs:
|
||||
await self._init_single_camera(cam_name)
|
||||
|
||||
async def _poll_config_updates(self) -> None:
|
||||
"""Poll for ONVIF config updates and re-initialize cameras as needed."""
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
try:
|
||||
updates = self.config_subscriber.check_for_updates()
|
||||
for update_type, cameras in updates.items():
|
||||
if update_type == CameraConfigUpdateEnum.onvif.name:
|
||||
for cam_name in cameras:
|
||||
await self._reinit_camera(cam_name)
|
||||
except Exception:
|
||||
logger.error("Error checking for ONVIF config updates")
|
||||
|
||||
async def _close_camera(self, cam_name: str) -> None:
|
||||
"""Close the ONVIF client session for a camera."""
|
||||
cam_state = self.cams.get(cam_name)
|
||||
if cam_state and "onvif" in cam_state:
|
||||
try:
|
||||
await cam_state["onvif"].close()
|
||||
except Exception:
|
||||
logger.debug(f"Error closing ONVIF session for {cam_name}")
|
||||
|
||||
async def _reinit_camera(self, cam_name: str) -> None:
|
||||
"""Re-initialize a camera after config change."""
|
||||
logger.info(f"Re-initializing ONVIF for {cam_name} due to config change")
|
||||
|
||||
# close existing session before re-init
|
||||
await self._close_camera(cam_name)
|
||||
|
||||
cam = self.config.cameras.get(cam_name)
|
||||
if not cam or not cam.onvif.host:
|
||||
# ONVIF removed from config, clean up
|
||||
self.cams.pop(cam_name, None)
|
||||
self.camera_configs.pop(cam_name, None)
|
||||
self.failed_cams.pop(cam_name, None)
|
||||
return
|
||||
|
||||
# update stored config and reset state
|
||||
self.camera_configs[cam_name] = cam
|
||||
if cam_name not in self.status_locks:
|
||||
self.status_locks[cam_name] = asyncio.Lock()
|
||||
self.cams.pop(cam_name, None)
|
||||
self.failed_cams.pop(cam_name, None)
|
||||
|
||||
await self._init_single_camera(cam_name)
|
||||
|
||||
async def _init_single_camera(self, cam_name: str) -> bool:
|
||||
"""Initialize a single camera by name.
|
||||
|
||||
@@ -118,6 +175,7 @@ class OnvifController:
|
||||
"active": False,
|
||||
"features": [],
|
||||
"presets": {},
|
||||
"profiles": [],
|
||||
}
|
||||
return True
|
||||
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||
@@ -161,22 +219,60 @@ class OnvifController:
|
||||
)
|
||||
return False
|
||||
|
||||
# build list of valid PTZ profiles
|
||||
valid_profiles = [
|
||||
p
|
||||
for p in profiles
|
||||
if p.VideoEncoderConfiguration
|
||||
and p.PTZConfiguration
|
||||
and (
|
||||
p.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None
|
||||
or p.PTZConfiguration.DefaultContinuousZoomVelocitySpace is not None
|
||||
)
|
||||
]
|
||||
|
||||
# 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'",
|
||||
camera_name,
|
||||
getattr(p, "Name", None),
|
||||
p.token,
|
||||
)
|
||||
|
||||
configured_profile = self.config.cameras[camera_name].onvif.profile
|
||||
profile = None
|
||||
for _, onvif_profile in enumerate(profiles):
|
||||
if (
|
||||
onvif_profile.VideoEncoderConfiguration
|
||||
and onvif_profile.PTZConfiguration
|
||||
and (
|
||||
onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace
|
||||
is not None
|
||||
or onvif_profile.PTZConfiguration.DefaultContinuousZoomVelocitySpace
|
||||
is not 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
|
||||
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,
|
||||
)
|
||||
):
|
||||
# use the first profile that has a valid ptz configuration
|
||||
profile = onvif_profile
|
||||
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}")
|
||||
break
|
||||
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:
|
||||
logger.error(
|
||||
@@ -184,6 +280,8 @@ class OnvifController:
|
||||
)
|
||||
return False
|
||||
|
||||
logger.debug(f"Selected Onvif profile for {camera_name}: {profile}")
|
||||
|
||||
# get the PTZ config for the profile
|
||||
try:
|
||||
configs = profile.PTZConfiguration
|
||||
@@ -218,48 +316,92 @@ class OnvifController:
|
||||
move_request.ProfileToken = profile.token
|
||||
self.cams[camera_name]["move_request"] = move_request
|
||||
|
||||
# extra setup for autotracking cameras
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
||||
):
|
||||
# get PTZ configuration options for feature detection and relative movement
|
||||
ptz_config = None
|
||||
fov_space_id = None
|
||||
|
||||
try:
|
||||
request = ptz.create_type("GetConfigurationOptions")
|
||||
request.ConfigurationToken = profile.PTZConfiguration.token
|
||||
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}"
|
||||
)
|
||||
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||
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(
|
||||
(
|
||||
i
|
||||
for i, space in enumerate(
|
||||
ptz_config.Spaces.RelativePanTiltTranslationSpace
|
||||
)
|
||||
if "TranslationSpaceFov" in space["URI"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
except (AttributeError, TypeError):
|
||||
fov_space_id = None
|
||||
|
||||
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.ProfileToken = profile.token
|
||||
self.cams[camera_name]["status_request"] = status_request
|
||||
|
||||
service_capabilities_request = ptz.create_type("GetServiceCapabilities")
|
||||
self.cams[camera_name]["service_capabilities_request"] = (
|
||||
service_capabilities_request
|
||||
)
|
||||
|
||||
fov_space_id = next(
|
||||
(
|
||||
i
|
||||
for i, space in enumerate(
|
||||
ptz_config.Spaces.RelativePanTiltTranslationSpace
|
||||
)
|
||||
if "TranslationSpaceFov" in space["URI"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
# status request for autotracking and filling ptz-parameters
|
||||
status_request = ptz.create_type("GetStatus")
|
||||
status_request.ProfileToken = profile.token
|
||||
self.cams[camera_name]["status_request"] = status_request
|
||||
# setup relative move request when FOV relative movement is supported
|
||||
if (
|
||||
fov_space_id is not None
|
||||
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
||||
):
|
||||
# one-off GetStatus to seed Translation field
|
||||
status = None
|
||||
try:
|
||||
status = await ptz.GetStatus(status_request)
|
||||
logger.debug(f"Onvif status config for {camera_name}: {status}")
|
||||
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}")
|
||||
status = None
|
||||
logger.warning(f"Unable to get status from camera {camera_name}: {e}")
|
||||
|
||||
# autotracking relative panning/tilting needs a relative zoom value set to 0
|
||||
# if camera supports relative movement
|
||||
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 (
|
||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
||||
!= ZoomingModeEnum.disabled
|
||||
autotracking_enabled
|
||||
and autotracking_config.zooming != ZoomingModeEnum.disabled
|
||||
):
|
||||
zoom_space_id = next(
|
||||
(
|
||||
@@ -271,60 +413,43 @@ class OnvifController:
|
||||
),
|
||||
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:
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
||||
!= ZoomingModeEnum.disabled
|
||||
):
|
||||
try:
|
||||
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"
|
||||
][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:
|
||||
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||
logger.warning(
|
||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.config.cameras[
|
||||
camera_name
|
||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
||||
logger.warning(
|
||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
||||
else:
|
||||
# remove zoom fields from relative move request
|
||||
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(
|
||||
f"{camera_name}: Relative move request after deleting zoom: {rel_move_request}"
|
||||
)
|
||||
|
||||
if move_request.Speed is None:
|
||||
move_request.Speed = configs.DefaultPTZSpeed if configs else None
|
||||
if rel_move_request.Speed is None:
|
||||
rel_move_request.Speed = configs.DefaultPTZSpeed if configs else None
|
||||
logger.debug(
|
||||
f"{camera_name}: Relative move request after setup: {move_request}"
|
||||
f"{camera_name}: Relative move request after setup: {rel_move_request}"
|
||||
)
|
||||
self.cams[camera_name]["relative_move_request"] = move_request
|
||||
self.cams[camera_name]["relative_move_request"] = rel_move_request
|
||||
|
||||
# setup absolute moving request for autotracking zooming
|
||||
move_request = ptz.create_type("AbsoluteMove")
|
||||
move_request.ProfileToken = profile.token
|
||||
self.cams[camera_name]["absolute_move_request"] = 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
|
||||
try:
|
||||
@@ -358,48 +483,48 @@ class OnvifController:
|
||||
|
||||
if configs.DefaultRelativeZoomTranslationSpace:
|
||||
supported_features.append("zoom-r")
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
||||
):
|
||||
if ptz_config is not None:
|
||||
try:
|
||||
# get camera's zoom limits from onvif config
|
||||
self.cams[camera_name]["relative_zoom_range"] = (
|
||||
ptz_config.Spaces.RelativeZoomTranslationSpace[0]
|
||||
)
|
||||
except Exception as e:
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
||||
== ZoomingModeEnum.relative
|
||||
):
|
||||
self.config.cameras[
|
||||
camera_name
|
||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
||||
if autotracking_config.zooming == ZoomingModeEnum.relative:
|
||||
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||
logger.warning(
|
||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
||||
)
|
||||
|
||||
if configs.DefaultAbsoluteZoomPositionSpace:
|
||||
supported_features.append("zoom-a")
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
||||
):
|
||||
if ptz_config is not None:
|
||||
try:
|
||||
# get camera's zoom limits from onvif config
|
||||
self.cams[camera_name]["absolute_zoom_range"] = (
|
||||
ptz_config.Spaces.AbsoluteZoomPositionSpace[0]
|
||||
)
|
||||
self.cams[camera_name]["zoom_limits"] = configs.ZoomLimits
|
||||
except Exception as e:
|
||||
if self.config.cameras[camera_name].onvif.autotracking.zooming:
|
||||
self.config.cameras[
|
||||
camera_name
|
||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
||||
if autotracking_config.zooming != ZoomingModeEnum.disabled:
|
||||
autotracking_config.zooming = ZoomingModeEnum.disabled
|
||||
logger.warning(
|
||||
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 (
|
||||
self.cams[camera_name]["video_source_token"] is not None
|
||||
and imaging is not None
|
||||
@@ -416,10 +541,9 @@ class OnvifController:
|
||||
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||
logger.debug(f"Focus not supported for {camera_name}: {e}")
|
||||
|
||||
# detect FOV relative movement support
|
||||
if (
|
||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
||||
and fov_space_id is not None
|
||||
fov_space_id is not None
|
||||
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
||||
):
|
||||
supported_features.append("pt-r-fov")
|
||||
@@ -548,11 +672,8 @@ class OnvifController:
|
||||
move_request.Translation.PanTilt.x = pan
|
||||
move_request.Translation.PanTilt.y = tilt
|
||||
|
||||
if (
|
||||
"zoom-r" in self.cams[camera_name]["features"]
|
||||
and self.config.cameras[camera_name].onvif.autotracking.zooming
|
||||
== ZoomingModeEnum.relative
|
||||
):
|
||||
# include zoom if requested and camera supports relative zoom
|
||||
if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
|
||||
move_request.Speed = {
|
||||
"PanTilt": {
|
||||
"x": speed,
|
||||
@@ -560,7 +681,7 @@ class OnvifController:
|
||||
},
|
||||
"Zoom": {"x": speed},
|
||||
}
|
||||
move_request.Translation.Zoom.x = zoom
|
||||
move_request["Translation"]["Zoom"] = {"x": zoom}
|
||||
|
||||
await self.cams[camera_name]["ptz"].RelativeMove(move_request)
|
||||
|
||||
@@ -568,12 +689,8 @@ class OnvifController:
|
||||
move_request.Translation.PanTilt.x = 0
|
||||
move_request.Translation.PanTilt.y = 0
|
||||
|
||||
if (
|
||||
"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
|
||||
if zoom != 0 and "zoom-r" in self.cams[camera_name]["features"]:
|
||||
del move_request["Translation"]["Zoom"]
|
||||
|
||||
self.cams[camera_name]["active"] = False
|
||||
|
||||
@@ -717,8 +834,18 @@ class OnvifController:
|
||||
elif command == OnvifCommandEnum.preset:
|
||||
await self._move_to_preset(camera_name, param)
|
||||
elif command == OnvifCommandEnum.move_relative:
|
||||
_, pan, tilt = param.split("_")
|
||||
await self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
|
||||
parts = param.split("_")
|
||||
if len(parts) == 3:
|
||||
_, pan, tilt = parts
|
||||
zoom = 0.0
|
||||
elif len(parts) == 4:
|
||||
_, pan, tilt, zoom = parts
|
||||
else:
|
||||
logger.error(f"Invalid move_relative params: {param}")
|
||||
return
|
||||
await self._move_relative(
|
||||
camera_name, float(pan), float(tilt), float(zoom), 1
|
||||
)
|
||||
elif command in (OnvifCommandEnum.zoom_in, OnvifCommandEnum.zoom_out):
|
||||
await self._zoom(camera_name, command)
|
||||
elif command in (OnvifCommandEnum.focus_in, OnvifCommandEnum.focus_out):
|
||||
@@ -773,6 +900,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:
|
||||
@@ -970,6 +1098,7 @@ class OnvifController:
|
||||
return
|
||||
|
||||
logger.info("Exiting ONVIF controller...")
|
||||
self.config_subscriber.stop()
|
||||
|
||||
def stop_and_cleanup():
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user