diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 17108e73091..1340c52459d 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -10,13 +10,15 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA + CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, + ATTR_ENTITY_ID) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, DOMAIN from homeassistant.components.ffmpeg import ( DATA_FFMPEG, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream) +from homeassistant.helpers.service import extract_entity_ids _LOGGER = logging.getLogger(__name__) @@ -32,6 +34,22 @@ DEFAULT_USERNAME = 'admin' DEFAULT_PASSWORD = '888888' DEFAULT_ARGUMENTS = '-q:v 2' +ATTR_PAN = "pan" +ATTR_TILT = "tilt" +ATTR_ZOOM = "zoom" + +DIR_UP = "UP" +DIR_DOWN = "DOWN" +DIR_LEFT = "LEFT" +DIR_RIGHT = "RIGHT" +ZOOM_OUT = "ZOOM_OUT" +ZOOM_IN = "ZOOM_IN" + +SERVICE_PTZ = "onvif_ptz" + +ONVIF_DATA = "onvif" +ENTITIES = "entities" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -41,12 +59,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, }) +SERVICE_PTZ_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, + ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT]), + ATTR_TILT: vol.In([DIR_UP, DIR_DOWN]), + ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN]) +}) + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up a ONVIF camera.""" if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)): return + + def 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) + all_cameras = hass.data[ONVIF_DATA][ENTITIES] + entity_ids = extract_entity_ids(hass, service) + target_cameras = [] + if not entity_ids: + target_cameras = all_cameras + else: + target_cameras = [camera for camera in all_cameras + if camera.entity_id in entity_ids] + for camera in target_cameras: + camera.perform_ptz(pan, tilt, zoom) + + hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz, + schema=SERVICE_PTZ_SCHEMA) async_add_devices([ONVIFHassCamera(hass, config)]) @@ -55,19 +99,21 @@ class ONVIFHassCamera(Camera): def __init__(self, hass, config): """Initialize a ONVIF camera.""" - from onvif import ONVIFCamera + from onvif import ONVIFCamera, exceptions super().__init__() self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._input = None + camera = None try: _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", config.get(CONF_HOST), config.get(CONF_PORT)) - media_service = ONVIFCamera( + camera = ONVIFCamera( config.get(CONF_HOST), config.get(CONF_PORT), config.get(CONF_USERNAME), config.get(CONF_PASSWORD) - ).create_media_service() + ) + media_service = camera.create_media_service() stream_uri = media_service.GetStreamUri( {'StreamSetup': {'Stream': 'RTP-Unicast', 'Transport': 'RTSP'}} ) @@ -81,6 +127,30 @@ class ONVIFHassCamera(Camera): except Exception as err: _LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) raise + try: + self._ptz = camera.create_ptz_service() + except exceptions.ONVIFError as err: + self._ptz = None + _LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) + + def perform_ptz(self, pan, tilt, zoom): + """Perform a PTZ action on the camera.""" + if self._ptz: + 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}}} + self._ptz.ContinuousMove(req) + + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + if ONVIF_DATA not in self.hass.data: + self.hass.data[ONVIF_DATA] = {} + self.hass.data[ONVIF_DATA][ENTITIES] = [] + self.hass.data[ONVIF_DATA][ENTITIES].append(self) @asyncio.coroutine def async_camera_image(self): diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 926af582cc7..b548f3d1ada 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -23,3 +23,20 @@ snapshot: filename: description: Template of a Filename. Variable is entity_id. example: '/tmp/snapshot_{{ entity_id }}' + +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" +