From 0d667c1bd9842125ddcd7817f0867c78b7868f40 Mon Sep 17 00:00:00 2001 From: olijouve <17448560+olijouve@users.noreply.github.com> Date: Fri, 6 Mar 2020 15:14:01 +0100 Subject: [PATCH] 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 --- homeassistant/components/camera/services.yaml | 15 -- homeassistant/components/onvif/camera.py | 139 ++++++++++++++---- homeassistant/components/onvif/services.yaml | 35 +++-- 3 files changed, 134 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index c50e2926a3fdc..6196322e23458 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -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" diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index f87da72936d92..02c9d2e95446b 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -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, @@ -48,6 +47,10 @@ 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_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 @@ 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_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 @@ -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( @@ -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 diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index 667538f056a7f..8d14633cc9c39 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -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"