8000 Add turn_on/off service to camera by awarecan · Pull Request #15051 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add turn_on/off service to camera #15051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
87 changes: 84 additions & 3 deletions homeassistant/components/camera/__init__.py
AE8F
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import voluptuous as vol

from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \
SERVICE_TURN_ON
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity
Expand Down Expand Up @@ -47,6 +48,9 @@
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'

# Bitfield of features supported by the camera entity
SUPPORT_ON_OFF = 1

DEFAULT_CONTENT_TYPE = 'image/jpeg'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'

Expand Down Expand Up @@ -79,6 +83,35 @@ class Image:
content = attr.ib(type=bytes)


@bind_hass
def turn_off(hass, entity_id=None):
"""Turn off camera."""
hass.add_job(async_turn_off, hass, entity_id)


@bind_hass
async def async_turn_off(hass, entity_id=None):
"""Turn off camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
Copy link
Member
@MartinHjelmare MartinHjelmare Jul 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This differs from how we set data on line 108 - 110. I think we should go with one of the ways, not have different logic in turn on vs turn off.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use turn_off version

await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)


@bind_hass
def turn_on(hass, entity_id=None):
"""Turn on camera."""
hass.add_job(async_turn_on, hass, entity_id)


@bind_hass
async def async_turn_on(hass, entity_id=None):
"""Turn on camera, and set operation mode."""
data = {}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id

await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)


@bind_hass
def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection."""
Expand Down Expand Up @@ -119,6 +152,9 @@ async def async_get_image(hass, entity_id, timeout=10):
if camera is None:
raise HomeAssistantError('Camera not found')

if not camera.is_on:
raise HomeAssistantError('Camera is off')

with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image()
Expand Down Expand Up @@ -163,6 +199,12 @@ async def async_handle_camera_service(service):
await camera.async_enable_motion_detection()
elif service.service == SERVICE_DISABLE_MOTION:
await camera.async_disable_motion_detection()
elif service.service == SERVICE_TURN_OFF and \
camera.supported_features & SUPPORT_ON_OFF:
await camera.async_turn_off()
elif service.service == SERVICE_TURN_ON and \
camera.supported_features & SUPPORT_ON_OFF:
await camera.async_turn_on()

if not camera.should_poll:
continue
Expand Down Expand Up @@ -200,6 +242,12 @@ def _write_image(to_file, image_data):
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)

hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA)
Expand Down Expand Up @@ -243,6 +291,11 @@ def entity_picture(self):
"""Return a link to the camera feed as entity picture."""
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])

@property
def supported_features(self):
"""Flag supported features."""
return 0

@property
def is_recording(self):
"""Return true if the device is recording."""
Expand Down Expand Up @@ -337,10 +390,34 @@ def state(self):
return STATE_STREAMING
return STATE_IDLE

@property
def is_on(self):
"""Return true if on."""
return True

def turn_off(self):
"""Turn off camera."""
raise NotImplementedError()

@callback
def async_turn_off(self):
"""Turn off camera."""
return self.hass.async_add_job(self.turn_off)

def turn_on(self):
"""Turn off camera."""
raise NotImplementedError()

@callback
def async_turn_on(self):
"""Turn off camera."""
return self.hass.async_add_job(self.turn_on)

def enable_motion_detection(self):
"""Enable motion detection in the camera."""
raise NotImplementedError()

@callback
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. This function returns a task that has to be awaited.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean I should not add callback, but should change the method signature to async def?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function used to return a task that needs to be awaited.

By labelling it callback, you tell people not to await it, meaning the function is never executed.

If you change it to async, it's more clear, but then you also need to add an await.

def async_enable_motion_detection(self):
"""Call the job and enable motion detection."""
return self.hass.async_add_job(self.enable_motion_detection)
Expand All @@ -349,6 +426,7 @@ def disable_motion_detection(self):
"""Disable motion detection in camera."""
raise NotImplementedError()

@callback
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. This function returns a task that has to be awaited.

def async_disable_motion_detection(self):
"""Call the job and disable motion detection."""
return self.hass.async_add_job(self.disable_motion_detection)
Expand Down Expand Up @@ -393,15 +471,18 @@ async def get(self, request, entity_id):
camera = self.component.get_entity(entity_id)

if camera is None:
status = 404 if request[KEY_AUTHENTICATED] else 401
return web.Response(status=status)
raise web.HTTPNotFound()

authenticated = (request[KEY_AUTHENTICATED] or
request.query.get('token') in camera.access_tokens)

if not authenticated:
raise web.HTTPUnauthorized()

if not camera.is_on:
_LOGGER.debug('Camera is off.')
raise web.HTTPServiceUnavailable()

return await self.handle(request, camera)

async def handle(self, request, camera):
Expand Down
48 changes: 38 additions & 10 deletions homeassistant/components/camera/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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
import os

from homeassistant.components.camera import Camera, SUPPORT_ON_OFF

_LOGGER = logging.getLogger(__name__)

Expand All @@ -16,26 +16,29 @@ async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Demo camera platform."""
async_add_devices([
DemoCamera(hass, config, 'Demo camera')
DemoCamera('Demo camera')
])


class DemoCamera(Camera):
"""The representation of a Demo camera."""

def __init__(self, hass, config, name):
def __init__(self, name):
"""Initialize demo camera component."""
super().__init__()
self._parent = hass
self._name = name
self._motion_status = False
self.is_streaming = True
self._images_index = 0

def camera_image(self):
"""Return a faked still image response."""
now = dt_util.utcnow()
self._images_index = (self._images_index + 1) % 4

image_path = os.path.join(
os.path.dirname(__file__), 'demo_{}.jpg'.format(now.second % 4))
os.path.dirname(__file__),
'demo_{}.jpg'.format(self._images_index))
_LOGGER.debug('Loading camera_image: %s', image_path)
with open(image_path, 'rb') as file:
return file.read()

Expand All @@ -46,8 +49,21 @@ def name(self):

@property
def should_poll(self):
"""Camera should poll periodically."""
return True
"""Demo camera doesn't need poll.

Need explicitly call schedule_update_ha_state() after state changed.
"""
return False

@property
def supported_features(self):
"""Camera support turn on/off features."""
return SUPPORT_ON_OFF

@property
def is_on(self):
"""Whether camera is on (streaming)."""
return self.is_streaming

@property
def motion_detection_enabled(self):
Expand All @@ -57,7 +73,19 @@ def motion_detection_enabled(self):
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
self.schedule_update_ha_state()

def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.schedule_update_ha_state()

def turn_off(self):
"""Turn off camera."""
self.is_streaming = False
self.schedule_update_ha_state()

def turn_on(self):
"""Turn on camera."""
self.is_streaming = True
self.schedule_update_ha_state()
14 changes: 14 additions & 0 deletions homeassistant/components/camera/services.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Describes the format for available camera services

turn_off:
description: Turn off camera.
fields:
entity_id:
description: Entity id.
example: 'camera.living_room'

turn_on:
description: Turn on camera.
fields:
entity_id:
description: Entity id.
example: 'camera.living_room'

enable_motion_detection:
description: Enable the motion detection in a camera.
fields:
Expand Down
85 changes: 80 additions & 5 deletions tests/components/camera/test_demo.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,89 @@
"""The tests for local file camera component."""
import asyncio
from unittest.mock import mock_open, patch, PropertyMock

import pytest

from homeassistant.components import camera
from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component


@asyncio.coroutine
def test_motion_detection(hass):
@pytest.fixture
def demo_camera(hass):
"""Initialize a demo camera platform."""
hass.loop.run_until_complete(async_setup_component(hass, 'camera', {
camera.DOMAIN: {
'platform': 'demo'
}
}))
return hass.data['camera'].get_entity('camera.demo_camera')


async def test_init_state_is_streaming(hass, demo_camera):
"""Demo camera initialize as streaming."""
assert demo_camera.state == STATE_STREAMING

mock_on_img = mock_open(read_data=b'ON')
wi 8000 th patch('homeassistant.components.camera.demo.open', mock_on_img,
create=True):
image = await camera.async_get_image(hass, demo_camera.entity_id)
assert mock_on_img.called
assert mock_on_img.call_args_list[0][0][0][-6:] \
in ['_0.jpg', '_1.jpg', '_2.jpg', '_3.jpg']
assert image.content == b'ON'


async def test_turn_on_state_back_to_streaming(hass, demo_camera):
"""After turn on state back to streaming."""
assert demo_camera.state == STATE_STREAMING
await camera.async_turn_off(hass, demo_camera.entity_id)
await hass.async_block_till_done()

assert demo_camera.state == STATE_IDLE

await camera.async_turn_on(hass, demo_camera.entity_id)
await hass.async_block_till_done()

assert demo_camera.state == STATE_STREAMING


async def test_turn_off_image(hass, demo_camera):
"""After turn off, Demo camera raise error."""
await camera.async_turn_off(hass, demo_camera.entity_id)
await hass.async_block_till_done()

with pytest.raises(HomeAssistantError) as error:
await camera.async_get_image(hass, demo_camera.entity_id)
assert error.args[0] == 'Camera is off'


async def test_turn_off_invalid_camera(hass, demo_camera):
"""Turn off non-exist camera should quietly fail."""
assert demo_camera.state == STATE_STREAMING
await camera.async_turn_off(hass, 'camera.invalid_camera')
await hass.async_block_till_done()

assert demo_camera.state == STATE_STREAMING


async def test_turn_off_unsupport_camera(hass, demo_camera):
"""Turn off unsupported camera should quietly fail."""
assert demo_camera.state == STATE_STREAMING
with patch('homeassistant.components.camera.demo.DemoCamera'
'.supported_features', new_callable=PropertyMock) as m:
m.return_value = 0

await camera.async_turn_off(hass, demo_camera.entity_id)
await hass.async_block_till_done()

assert demo_camera.state == STATE_STREAMING


async def test_motion_detection(hass):
"""Test motion detection services."""
# Setup platform
yield from async_setup_component(hass, 'camera', {
await async_setup_component(hass, 'camera', {
'camera': {
'platform': 'demo'
}
Expand All @@ -20,7 +95,7 @@ def test_motion_detection(hass):

# Call service to turn on motion detection
camera.enable_motion_detection(hass, 'camera.demo_camera')
yield from hass.async_block_till_done()
await hass.async_block_till_done()

# Check if state has been updated.
state = hass.states.get('camera.demo_camera')
Expand Down
0