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"