diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c509d582e11..110f9a11852 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -29,18 +29,22 @@ from homeassistant.helpers.entity import Entity 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 +DOMAIN = 'camera' +DEPENDENCIES = ['http'] + _LOGGER = logging.getLogger(__name__) -SERVICE_EN_MOTION = 'enable_motion_detection' -SERVICE_DISEN_MOTION = 'disable_motion_detection' -DOMAIN = 'camera' -DEPENDENCIES = ['http'] +SERVICE_ENABLE_MOTION = 'enable_motion_detection' +SERVICE_DISABLE_MOTION = 'disable_motion_detection' +SERVICE_SNAPSHOT = 'snapshot' + SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + '.{}' +ATTR_FILENAME = 'filename' + STATE_RECORDING = 'recording' STATE_STREAMING = 'streaming' STATE_IDLE = 'idle' @@ -55,13 +59,17 @@ CAMERA_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) +CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FILENAME): cv.template +}) + @bind_hass 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)) + DOMAIN, SERVICE_ENABLE_MOTION, data)) @bind_hass @@ -69,9 +77,20 @@ 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)) + DOMAIN, SERVICE_DISABLE_MOTION, data)) +@bind_hass +def async_snapshot(hass, filename, entity_id=None): + """Make a snapshot from a camera.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_FILENAME] = filename + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SNAPSHOT, data)) + + +@bind_hass @asyncio.coroutine def async_get_image(hass, entity_id, timeout=10): """Fetch a image from a camera entity.""" @@ -119,7 +138,8 @@ def async_setup(hass, config): entity.async_update_token() hass.async_add_job(entity.async_update_ha_state()) - async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL) + hass.helpers.event.async_track_time_interval( + update_tokens, TOKEN_CHANGE_INTERVAL) @asyncio.coroutine def async_handle_camera_service(service): @@ -128,9 +148,9 @@ def async_setup(hass, config): update_tasks = [] for camera in target_cameras: - if service.service == SERVICE_EN_MOTION: + if service.service == SERVICE_ENABLE_MOTION: yield from camera.async_enable_motion_detection() - elif service.service == SERVICE_DISEN_MOTION: + elif service.service == SERVICE_DISABLE_MOTION: yield from camera.async_disable_motion_detection() if not camera.should_poll: @@ -140,16 +160,50 @@ def async_setup(hass, config): if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) + @asyncio.coroutine + def async_handle_snapshot_service(service): + """Handle snapshot services calls.""" + target_cameras = component.async_extract_from_service(service) + filename = service.data[ATTR_FILENAME] + filename.hass = hass + + for camera in target_cameras: + snapshot_file = filename.async_render( + variables={ATTR_ENTITY_ID: camera}) + + # check if we allow to access to that file + if not hass.config.is_allowed_path(snapshot_file): + _LOGGER.error( + "Can't write %s, no access to path!", snapshot_file) + continue + + image = yield from camera.async_camera_image() + + def _write_image(to_file, image_data): + """Executor helper to write image.""" + with open(to_file, 'wb') as img_file: + img_file.write(image_data) + + try: + yield from hass.async_add_job( + _write_image, snapshot_file, image) + except OSError as err: + _LOGGER.error("Can't write image to file: %s", err) + 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) + DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service, + descriptions.get(SERVICE_ENABLE_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) + DOMAIN, SERVICE_DISABLE_MOTION, async_handle_camera_service, + descriptions.get(SERVICE_DISABLE_MOTION), schema=CAMERA_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SNAPSHOT, async_handle_snapshot_service, + descriptions.get(SERVICE_SNAPSHOT), + schema=CAMERA_SERVICE_SNAPSHOT) return True diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index b6ed22f708a..606ef076746 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -10,8 +10,20 @@ enable_motion_detection: 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' + +snapshot: + description: Take a snapshot from a camera + + fields: + entity_id: + description: Name(s) of entities to disable motion detection + example: 'camera.living_room_camera' + + filename: + description: Template of a Filename. Variable is entity_id + example: '/tmp/snapshot_{{ entity_id }}' diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 97f6c0385df..70e95dd7b93 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,10 +1,10 @@ """The tests for the camera component.""" import asyncio -from unittest.mock import patch +from unittest.mock import patch, mock_open import pytest -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE import homeassistant.components.camera as camera import homeassistant.components.http as http @@ -15,6 +15,20 @@ from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) +@pytest.fixture +def mock_camera(hass): + """Initialize a demo camera platform.""" + assert hass.loop.run_until_complete(async_setup_component(hass, 'camera', { + camera.DOMAIN: { + 'platform': 'demo' + } + })) + + with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', + return_value=b'Test'): + yield + + class TestSetupCamera(object): """Test class for setup camera.""" @@ -105,3 +119,20 @@ class TestGetImage(object): self.hass, 'camera.demo_camera'), self.hass.loop).result() assert len(aioclient_mock.mock_calls) == 1 + + +@asyncio.coroutine +def test_snapshot_service(hass, mock_camera): + """Test snapshot service.""" + mopen = mock_open() + + with patch('homeassistant.components.camera.open', mopen, create=True), \ + patch.object(hass.config, 'is_allowed_path', + return_value=True): + hass.components.camera.async_snapshot('/tmp/bla') + yield from hass.async_block_till_done() + + mock_write = mopen().write + + assert len(mock_write.mock_calls) == 1 + assert mock_write.mock_calls[0][1][0] == b'Test'