mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add more onvif PTZ move modes (#30152)
* Adding support for PTZ move modes Adding support for other PTZ move modes. Onvif intergration used to only support RelativeMove where it should also supports AbsoluteMove, ContinuousMove and Stop. For exemple Goke GK7102 based IP camera only support ContinuousMove mode. This commit add those new modes with avaibility to select mode and params in service call. * Adding support for PTZ move modes Adding support for other PTZ move modes. Onvif intergration used to only support RelativeMove where it should also supports AbsoluteMove, ContinuousMove and Stop. For exemple Goke GK7102 based IP camera only support ContinuousMove mode. Update service helper for new avaibility to select mode and params in service call. * RelativeMode as default move_mode to avoid breakchange RelativeMode as default move_mode to avoid breakchange * add missing attribute add missing continuous_duration attribute * change service attribute label for continuous_duration * update description fix wrong assertion for move_mode attr description * Update services.yaml * Update services.yaml fix wrong wording for move_mode * Update camera.py Using defined constants instead of raw strings in conditions * Update camera.py Replace integer to floating point in logger debug PTZ values * Update services.yaml * Update services.yaml * Update camera.py * Update camera.py * use dict[key] for required schema keys and keys with default schema values * remove async for setup_ptz method * lint error * remove unecessary PTZ_NONE = "NONE" changed request by @MartinHjelmare * addressing @ MartinHjelmare comments - Remove None in defaluts and dicts - Replace long if blocks * remove NONE * lint issue * Update camera.py * Fix lint error - typo * rename onvif_ptz service to just ptz * rename onvif_ptz service to just ptz * use dict[key] when default values are set use service.data[key] instead of service.data.get[key] when default value is set in service schema * adresse comment: use dict[key] for pan tilt zoom * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
3b75fdccfd
commit
0d667c1bd9
@ -68,18 +68,3 @@ record:
|
|||||||
description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream.
|
description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream.
|
||||||
example: 4
|
example: 4
|
||||||
|
|
||||||
onvif_ptz:
|
|
||||||
description: Pan/Tilt/Zoom service for ONVIF camera.
|
|
||||||
fields:
|
|
||||||
entity_id:
|
|
||||||
description: Name(s) of entities to pan, tilt or zoom.
|
|
||||||
example: 'camera.living_room_camera'
|
|
||||||
pan:
|
|
||||||
description: "Direction of pan. Allowed values: LEFT, RIGHT."
|
|
||||||
example: 'LEFT'
|
|
||||||
tilt:
|
|
||||||
description: "Direction of tilt. Allowed values: DOWN, UP."
|
|
||||||
example: 'DOWN'
|
|
||||||
zoom:
|
|
||||||
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
|
||||||
example: "ZOOM_IN"
|
|
||||||
|
@ -15,7 +15,6 @@ from zeep.asyncio import AsyncTransport
|
|||||||
from zeep.exceptions import Fault
|
from zeep.exceptions import Fault
|
||||||
|
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
||||||
from homeassistant.components.camera.const import DOMAIN
|
|
||||||
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
|
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -48,6 +47,10 @@ CONF_PROFILE = "profile"
|
|||||||
ATTR_PAN = "pan"
|
ATTR_PAN = "pan"
|
||||||
ATTR_TILT = "tilt"
|
ATTR_TILT = "tilt"
|
||||||
ATTR_ZOOM = "zoom"
|
ATTR_ZOOM = "zoom"
|
||||||
|
ATTR_DISTANCE = "distance"
|
||||||
|
ATTR_SPEED = "speed"
|
||||||
|
ATTR_MOVE_MODE = "move_mode"
|
||||||
|
ATTR_CONTINUOUS_DURATION = "continuous_duration"
|
||||||
|
|
||||||
DIR_UP = "UP"
|
DIR_UP = "UP"
|
||||||
DIR_DOWN = "DOWN"
|
DIR_DOWN = "DOWN"
|
||||||
@ -55,13 +58,20 @@ DIR_LEFT = "LEFT"
|
|||||||
DIR_RIGHT = "RIGHT"
|
DIR_RIGHT = "RIGHT"
|
||||||
ZOOM_OUT = "ZOOM_OUT"
|
ZOOM_OUT = "ZOOM_OUT"
|
||||||
ZOOM_IN = "ZOOM_IN"
|
ZOOM_IN = "ZOOM_IN"
|
||||||
PTZ_NONE = "NONE"
|
PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1}
|
||||||
|
TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1}
|
||||||
|
ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1}
|
||||||
|
CONTINUOUS_MOVE = "ContinuousMove"
|
||||||
|
RELATIVE_MOVE = "RelativeMove"
|
||||||
|
ABSOLUTE_MOVE = "AbsoluteMove"
|
||||||
|
|
||||||
SERVICE_PTZ = "onvif_ptz"
|
SERVICE_PTZ = "ptz"
|
||||||
|
|
||||||
|
DOMAIN = "onvif"
|
||||||
ONVIF_DATA = "onvif"
|
ONVIF_DATA = "onvif"
|
||||||
ENTITIES = "entities"
|
ENTITIES = "entities"
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
@ -79,9 +89,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
SERVICE_PTZ_SCHEMA = vol.Schema(
|
SERVICE_PTZ_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT, PTZ_NONE]),
|
vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]),
|
||||||
ATTR_TILT: vol.In([DIR_UP, DIR_DOWN, PTZ_NONE]),
|
vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]),
|
||||||
ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN, PTZ_NONE]),
|
vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]),
|
||||||
|
ATTR_MOVE_MODE: vol.In([CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE]),
|
||||||
|
vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float,
|
||||||
|
vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float,
|
||||||
|
vol.Optional(ATTR_SPEED, default=0.5): cv.small_float,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,9 +106,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
|
|
||||||
async def async_handle_ptz(service):
|
async def async_handle_ptz(service):
|
||||||
"""Handle PTZ service call."""
|
"""Handle PTZ service call."""
|
||||||
pan = service.data.get(ATTR_PAN, None)
|
pan = service.data.get(ATTR_PAN)
|
||||||
tilt = service.data.get(ATTR_TILT, None)
|
tilt = service.data.get(ATTR_TILT)
|
||||||
zoom = service.data.get(ATTR_ZOOM, None)
|
zoom = service.data.get(ATTR_ZOOM)
|
||||||
|
distance = service.data[ATTR_DISTANCE]
|
||||||
|
speed = service.data[ATTR_SPEED]
|
||||||
|
move_mode = service.data.get(ATTR_MOVE_MODE)
|
||||||
|
continuous_duration = service.data[ATTR_CONTINUOUS_DURATION]
|
||||||
all_cameras = hass.data[ONVIF_DATA][ENTITIES]
|
all_cameras = hass.data[ONVIF_DATA][ENTITIES]
|
||||||
entity_ids = await async_extract_entity_ids(hass, service)
|
entity_ids = await async_extract_entity_ids(hass, service)
|
||||||
target_cameras = []
|
target_cameras = []
|
||||||
@ -105,7 +123,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
camera for camera in all_cameras if camera.entity_id in entity_ids
|
camera for camera in all_cameras if camera.entity_id in entity_ids
|
||||||
]
|
]
|
||||||
for camera in target_cameras:
|
for camera in target_cameras:
|
||||||
await camera.async_perform_ptz(pan, tilt, zoom)
|
await camera.async_perform_ptz(
|
||||||
|
pan, tilt, zoom, distance, speed, move_mode, continuous_duration
|
||||||
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
|
DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
|
||||||
@ -263,6 +283,35 @@ class ONVIFHassCamera(Camera):
|
|||||||
"Couldn't get camera '%s' date/time. Error: %s", self._name, err
|
"Couldn't get camera '%s' date/time. Error: %s", self._name, err
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_obtain_profile_token(self):
|
||||||
|
"""Obtain profile token to use with requests."""
|
||||||
|
try:
|
||||||
|
media_service = self._camera.get_service("media")
|
||||||
|
|
||||||
|
profiles = await media_service.GetProfiles()
|
||||||
|
|
||||||
|
_LOGGER.debug("Retrieved '%d' profiles", len(profiles))
|
||||||
|
|
||||||
|
if self._profile_index >= len(profiles):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"ONVIF Camera '%s' doesn't provide profile %d."
|
||||||
|
" Using the last profile.",
|
||||||
|
self._name,
|
||||||
|
self._profile_index,
|
||||||
|
)
|
||||||
|
self._profile_index = -1
|
||||||
|
|
||||||
|
_LOGGER.debug("Using profile index '%d'", self._profile_index)
|
||||||
|
|
||||||
|
return profiles[self._profile_index].token
|
||||||
|
except exceptions.ONVIFError as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Couldn't retrieve profile token of camera '%s'. Error: %s",
|
||||||
|
self._name,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
async def async_obtain_input_uri(self):
|
async def async_obtain_input_uri(self):
|
||||||
"""Set the input uri for the camera."""
|
"""Set the input uri for the camera."""
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -320,37 +369,67 @@ class ONVIFHassCamera(Camera):
|
|||||||
def setup_ptz(self):
|
def setup_ptz(self):
|
||||||
"""Set up PTZ if available."""
|
"""Set up PTZ if available."""
|
||||||
_LOGGER.debug("Setting up the ONVIF PTZ service")
|
_LOGGER.debug("Setting up the ONVIF PTZ service")
|
||||||
if self._camera.get_service("ptz", create=False) is None:
|
if self._camera.get_service("ptz") is None:
|
||||||
_LOGGER.debug("PTZ is not available")
|
_LOGGER.debug("PTZ is not available")
|
||||||
else:
|
else:
|
||||||
self._ptz_service = self._camera.create_ptz_service()
|
self._ptz_service = self._camera.create_ptz_service()
|
||||||
_LOGGER.debug("Completed set up of the ONVIF camera component")
|
_LOGGER.debug("Completed set up of the ONVIF camera component")
|
||||||
|
|
||||||
async def async_perform_ptz(self, pan, tilt, zoom):
|
async def async_perform_ptz(
|
||||||
|
self, pan, tilt, zoom, distance, speed, move_mode, continuous_duration
|
||||||
|
):
|
||||||
"""Perform a PTZ action on the camera."""
|
"""Perform a PTZ action on the camera."""
|
||||||
if self._ptz_service is None:
|
if self._ptz_service is None:
|
||||||
_LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name)
|
_LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._ptz_service:
|
if self._ptz_service:
|
||||||
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
|
pan_val = distance * PAN_FACTOR.get(pan, 0)
|
||||||
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
|
tilt_val = distance * TILT_FACTOR.get(tilt, 0)
|
||||||
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
|
zoom_val = distance * ZOOM_FACTOR.get(zoom, 0)
|
||||||
req = {
|
speed_val = speed
|
||||||
"Velocity": {
|
_LOGGER.debug(
|
||||||
"PanTilt": {"_x": pan_val, "_y": tilt_val},
|
"Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f",
|
||||||
"Zoom": {"_x": zoom_val},
|
move_mode,
|
||||||
}
|
pan_val,
|
||||||
}
|
tilt_val,
|
||||||
|
zoom_val,
|
||||||
|
speed_val,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug(
|
req = self._ptz_service.create_type(move_mode)
|
||||||
"Calling PTZ | Pan = %d | Tilt = %d | Zoom = %d",
|
req.ProfileToken = await self.async_obtain_profile_token()
|
||||||
pan_val,
|
if move_mode == CONTINUOUS_MOVE:
|
||||||
tilt_val,
|
req.Velocity = {
|
||||||
zoom_val,
|
"PanTilt": {"x": pan_val, "y": tilt_val},
|
||||||
)
|
"Zoom": {"x": zoom_val},
|
||||||
|
}
|
||||||
|
|
||||||
await self._ptz_service.ContinuousMove(req)
|
await self._ptz_service.ContinuousMove(req)
|
||||||
|
await asyncio.sleep(continuous_duration)
|
||||||
|
req = self._ptz_service.create_type("Stop")
|
||||||
|
req.ProfileToken = await self.async_obtain_profile_token()
|
||||||
|
await self._ptz_service.Stop({"ProfileToken": req.ProfileToken})
|
||||||
|
elif move_mode == RELATIVE_MOVE:
|
||||||
|
req.Translation = {
|
||||||
|
"PanTilt": {"x": pan_val, "y": tilt_val},
|
||||||
|
"Zoom": {"x": zoom_val},
|
||||||
|
}
|
||||||
|
req.Speed = {
|
||||||
|
"PanTilt": {"x": speed_val, "y": speed_val},
|
||||||
|
"Zoom": {"x": speed_val},
|
||||||
|
}
|
||||||
|
await self._ptz_service.RelativeMove(req)
|
||||||
|
elif move_mode == ABSOLUTE_MOVE:
|
||||||
|
req.Position = {
|
||||||
|
"PanTilt": {"x": pan_val, "y": tilt_val},
|
||||||
|
"Zoom": {"x": zoom_val},
|
||||||
|
}
|
||||||
|
req.Speed = {
|
||||||
|
"PanTilt": {"x": speed_val, "y": speed_val},
|
||||||
|
"Zoom": {"x": speed_val},
|
||||||
|
}
|
||||||
|
await self._ptz_service.AbsoluteMove(req)
|
||||||
except exceptions.ONVIFError as err:
|
except exceptions.ONVIFError as err:
|
||||||
if "Bad Request" in err.reason:
|
if "Bad Request" in err.reason:
|
||||||
self._ptz_service = None
|
self._ptz_service = None
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
onvif_ptz:
|
ptz:
|
||||||
description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera.
|
description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: 'String or list of strings that point at entity_ids of cameras. Else targets all.'
|
description: "String or list of strings that point at entity_ids of cameras. Else targets all."
|
||||||
example: 'camera.backyard'
|
example: "camera.living_room_camera"
|
||||||
tilt:
|
tilt:
|
||||||
description: 'Tilt direction. Allowed values: UP, DOWN, NONE'
|
description: "Tilt direction. Allowed values: UP, DOWN"
|
||||||
example: 'UP'
|
example: "UP"
|
||||||
pan:
|
pan:
|
||||||
description: 'Pan direction. Allowed values: RIGHT, LEFT, NONE'
|
description: "Pan direction. Allowed values: RIGHT, LEFT"
|
||||||
example: 'RIGHT'
|
example: "RIGHT"
|
||||||
zoom:
|
zoom:
|
||||||
description: 'Zoom. Allowed values: ZOOM_IN, ZOOM_OUT, NONE'
|
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
|
||||||
example: 'NONE'
|
example: "ZOOM_IN"
|
||||||
|
distance:
|
||||||
|
description: "Distance coefficient. Sets how much PTZ should be executed in one request. Allowed values: floating point numbers, 0 to 1"
|
||||||
|
default: 0.1
|
||||||
|
example: 0.1
|
||||||
|
speed:
|
||||||
|
description: "Speed coefficient. Sets how fast PTZ will be executed. Allowed values: floating point numbers, 0 to 1"
|
||||||
|
default: 0.5
|
||||||
|
example: 0.5
|
||||||
|
continuous_duration:
|
||||||
|
description: "Set ContinuousMove delay in seconds before stopping the move"
|
||||||
|
default: 0.5
|
||||||
|
example: 0.5
|
||||||
|
move_mode:
|
||||||
|
description: "PTZ moving mode. One of ContinuousMove, RelativeMove or AbsoluteMove"
|
||||||
|
default: "RelativeMove"
|
||||||
|
example: "ContinuousMove"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user