diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 952f4378598..c84421f50ea 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -12,13 +12,16 @@ from datetime import timedelta import logging import hashlib from random import SystemRandom +import os import aiohttp from aiohttp import web import async_timeout +import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) +from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -26,9 +29,12 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.helpers.event import async_track_time_interval +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +SERVICE_EN_MOTION = 'enable_motion_detection' +SERVICE_DISEN_MOTION = 'disable_motion_detection' DOMAIN = 'camera' DEPENDENCIES = ['http'] SCAN_INTERVAL = timedelta(seconds=30) @@ -44,6 +50,24 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) _RND = SystemRandom() +CAMERA_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + + +def enable_motion_detection(hass, entity_id=None): + """Enable Motion Detection.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_EN_MOTION, data)) + + +def disable_motion_detection(hass, entity_id=None): + """Disable Motion Detection.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_DISEN_MOTION, data)) + @asyncio.coroutine def async_get_image(hass, entity_id, timeout=10): @@ -93,6 +117,44 @@ def async_setup(hass, config): hass.async_add_job(entity.async_update_ha_state()) async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL) + + @asyncio.coroutine + def async_handle_camera_service(service): + """Handle calls to the camera services.""" + target_cameras = component.async_extract_from_service(service) + + for camera in target_cameras: + if service.service == SERVICE_EN_MOTION: + yield from camera.async_enable_motion_detection() + elif service.service == SERVICE_DISEN_MOTION: + yield from camera.async_disable_motion_detection() + + update_tasks = [] + for camera in target_cameras: + if not camera.should_poll: + continue + + update_coro = hass.async_add_job( + camera.async_update_ha_state(True)) + if hasattr(camera, 'async_update'): + update_tasks.append(update_coro) + else: + yield from update_coro + + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service, + descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service, + descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA) + return True @@ -126,6 +188,11 @@ class Camera(Entity): """Return the camera brand.""" return None + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return None + @property def model(self): """Return the camera model.""" @@ -202,6 +269,22 @@ class Camera(Entity): else: return STATE_IDLE + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + raise NotImplementedError() + + def async_enable_motion_detection(self): + """Call the job and enable motion detection.""" + return self.hass.async_add_job(self.enable_motion_detection) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + raise NotImplementedError() + + def async_disable_motion_detection(self): + """Call the job and disable motion detection.""" + return self.hass.async_add_job(self.disable_motion_detection) + @property def state_attributes(self): """Return the camera state attributes.""" @@ -215,6 +298,9 @@ class Camera(Entity): if self.brand: attr['brand'] = self.brand + if self.motion_detection_enabled: + attr['motion_detection'] = self.motion_detection_enabled + return attr @callback diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py index 2fd7d372eb5..be6aab30af5 100644 --- a/homeassistant/components/camera/arlo.py +++ b/homeassistant/components/camera/arlo.py @@ -9,17 +9,19 @@ import logging import voluptuous as vol +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream DEPENDENCIES = ['arlo', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' +ARLO_MODE_ARMED = 'armed' +ARLO_MODE_DISARMED = 'disarmed' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, @@ -46,9 +48,10 @@ class ArloCam(Camera): def __init__(self, hass, camera, device_info): """Initialize an Arlo camera.""" super().__init__() - self._camera = camera + self._base_stn = hass.data['arlo'].base_stations[0] self._name = self._camera.name + self._motion_status = False self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) @@ -87,3 +90,27 @@ class ArloCam(Camera): def brand(self): """Camera brand.""" return DEFAULT_BRAND + + @property + def should_poll(self): + """Camera should poll periodically.""" + return True + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return self._motion_status + + def set_base_station_mode(self, mode): + """Set the mode in the base station.""" + self._base_stn.mode = mode + + def enable_motion_detection(self): + """Enable the Motion detection in base station (Arm).""" + self._motion_status = True + self.set_base_station_mode(ARLO_MODE_ARMED) + + def disable_motion_detection(self): + """Disable the motion detection in base station (Disarm).""" + self._motion_status = False + self.set_base_station_mode(ARLO_MODE_DISARMED) diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py index 158f6c11751..d009f156e9d 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ import os - +import logging import homeassistant.util.dt as dt_util from homeassistant.components.camera import Camera +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo camera platform.""" add_devices([ - DemoCamera('Demo camera') + DemoCamera(hass, config, 'Demo camera') ]) class DemoCamera(Camera): """The representation of a Demo camera.""" - def __init__(self, name): + def __init__(self, hass, config, name): """Initialize demo camera component.""" super().__init__() + self._parent = hass self._name = name + self._motion_status = False def camera_image(self): """Return a faked still image response.""" @@ -38,3 +42,21 @@ class DemoCamera(Camera): def name(self): """Return the name of this camera.""" return self._name + + @property + def should_poll(self): + """Camera should poll periodically.""" + return True + + @property + def motion_detection_enabled(self): + """Camera Motion Detection Status.""" + return self._motion_status + + def enable_motion_detection(self): + """Enable the Motion detection in base station (Arm).""" + self._motion_status = True + + def disable_motion_detection(self): + """Disable the motion detection in base station (Disarm).""" + self._motion_status = False diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml new file mode 100644 index 00000000000..b6ed22f708a --- /dev/null +++ b/homeassistant/components/camera/services.yaml @@ -0,0 +1,17 @@ +# Describes the format for available camera services + +enable_motion_detection: + description: Enable the motion detection in a camera + + fields: + entity_id: + description: Name(s) of entities to enable motion detection + example: 'camera.living_room_camera' + +disable_motion_detection: + description: Disable the motion detection in a camera + + fields: + entity_id: + description: Name(s) of entities to disable motion detection + example: 'camera.living_room_camera' diff --git a/tests/components/camera/test_demo.py b/tests/components/camera/test_demo.py new file mode 100644 index 00000000000..51e04fca351 --- /dev/null +++ b/tests/components/camera/test_demo.py @@ -0,0 +1,27 @@ +"""The tests for local file camera component.""" +import asyncio +from homeassistant.components import camera +from homeassistant.setup import async_setup_component + + +@asyncio.coroutine +def test_motion_detection(hass): + """Test motion detection services.""" + # Setup platform + yield from async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'demo' + } + }) + + # Fetch state and check motion detection attribute + state = hass.states.get('camera.demo_camera') + assert not state.attributes.get('motion_detection') + + # Call service to turn on motion detection + camera.enable_motion_detection(hass, 'camera.demo_camera') + yield from hass.async_block_till_done() + + # Check if state has been updated. + state = hass.states.get('camera.demo_camera') + assert state.attributes.get('motion_detection')