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:
olijouve 2020-03-06 15:14:01 +01:00 committed by GitHub
parent 3b75fdccfd
commit 0d667c1bd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 133 additions and 54 deletions

View File

@ -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.
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"

View File

@ -15,7 +15,6 @@ from zeep.asyncio import AsyncTransport
from zeep.exceptions import Fault
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.const import (
ATTR_ENTITY_ID,
@ -48,6 +47,10 @@ CONF_PROFILE = "profile"
ATTR_PAN = "pan"
ATTR_TILT = "tilt"
ATTR_ZOOM = "zoom"
ATTR_DISTANCE = "distance"
ATTR_SPEED = "speed"
ATTR_MOVE_MODE = "move_mode"
ATTR_CONTINUOUS_DURATION = "continuous_duration"
DIR_UP = "UP"
DIR_DOWN = "DOWN"
@ -55,13 +58,20 @@ DIR_LEFT = "LEFT"
DIR_RIGHT = "RIGHT"
ZOOM_OUT = "ZOOM_OUT"
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"
ENTITIES = "entities"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@ -79,9 +89,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
SERVICE_PTZ_SCHEMA = vol.Schema(
{
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT, PTZ_NONE]),
ATTR_TILT: vol.In([DIR_UP, DIR_DOWN, PTZ_NONE]),
ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN, PTZ_NONE]),
vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]),
vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]),
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):
"""Handle PTZ service call."""
pan = service.data.get(ATTR_PAN, None)
tilt = service.data.get(ATTR_TILT, None)
zoom = service.data.get(ATTR_ZOOM, None)
pan = service.data.get(ATTR_PAN)
tilt = service.data.get(ATTR_TILT)
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]
entity_ids = await async_extract_entity_ids(hass, service)
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
]
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(
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
)
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):
"""Set the input uri for the camera."""
_LOGGER.debug(
@ -320,37 +369,67 @@ class ONVIFHassCamera(Camera):
def setup_ptz(self):
"""Set up PTZ if available."""
_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")
else:
self._ptz_service = self._camera.create_ptz_service()
_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."""
if self._ptz_service is None:
_LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name)
return
if self._ptz_service:
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
req = {
"Velocity": {
"PanTilt": {"_x": pan_val, "_y": tilt_val},
"Zoom": {"_x": zoom_val},
}
}
try:
pan_val = distance * PAN_FACTOR.get(pan, 0)
tilt_val = distance * TILT_FACTOR.get(tilt, 0)
zoom_val = distance * ZOOM_FACTOR.get(zoom, 0)
speed_val = speed
_LOGGER.debug(
"Calling PTZ | Pan = %d | Tilt = %d | Zoom = %d",
"Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f",
move_mode,
pan_val,
tilt_val,
zoom_val,
speed_val,
)
try:
req = self._ptz_service.create_type(move_mode)
req.ProfileToken = await self.async_obtain_profile_token()
if move_mode == CONTINUOUS_MOVE:
req.Velocity = {
"PanTilt": {"x": pan_val, "y": tilt_val},
"Zoom": {"x": zoom_val},
}
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:
if "Bad Request" in err.reason:
self._ptz_service = None

View File

@ -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.
fields:
entity_id:
description: 'String or list of strings that point at entity_ids of cameras. Else targets all.'
example: 'camera.backyard'
description: "String or list of strings that point at entity_ids of cameras. Else targets all."
example: "camera.living_room_camera"
tilt:
description: 'Tilt direction. Allowed values: UP, DOWN, NONE'
example: 'UP'
description: "Tilt direction. Allowed values: UP, DOWN"
example: "UP"
pan:
description: 'Pan direction. Allowed values: RIGHT, LEFT, NONE'
example: 'RIGHT'
description: "Pan direction. Allowed values: RIGHT, LEFT"
example: "RIGHT"
zoom:
description: 'Zoom. Allowed values: ZOOM_IN, ZOOM_OUT, NONE'
example: 'NONE'
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
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"