Skip to content

Commit

Permalink
Add more onvif PTZ move modes (home-assistant#30152)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
olijouve and MartinHjelmare authored Mar 6, 2020
1 parent 3b75fdc commit 0d667c1
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 55 deletions.
15 changes: 0 additions & 15 deletions homeassistant/components/camera/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
139 changes: 109 additions & 30 deletions homeassistant/components/onvif/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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,
Expand Down Expand Up @@ -48,20 +47,31 @@
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"
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,
Expand All @@ -79,9 +89,13 @@
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,
}
)

Expand All @@ -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 = []
Expand All @@ -105,7 +123,9 @@ async def async_handle_ptz(service):
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
Expand Down Expand Up @@ -263,6 +283,35 @@ async def async_check_date_and_time(self):
"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(
Expand Down Expand Up @@ -320,37 +369,67 @@ async def async_obtain_input_uri(self):
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")
_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},
}
}
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 %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:
_LOGGER.debug(
"Calling PTZ | Pan = %d | Tilt = %d | Zoom = %d",
pan_val,
tilt_val,
zoom_val,
)

await self._ptz_service.ContinuousMove(req)
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
Expand Down
35 changes: 25 additions & 10 deletions homeassistant/components/onvif/services.yaml
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 0d667c1

Please sign in to comment.