Camera services arm disarm including Netgear Arlo (#7961)

* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Added camera service calls to arm/disarm the cameras. Entity id is optional so that with a single call we can arm all the cameras or specify a particular entity id to arm if applicable and possible in that camera type.

* Fixed the spaces and indentation related issues that houndci found

* Fixed the spaces and indentation related issues that houndci found

* Missed the const file which has the macros defined.

* Fixed the CI build error

* Fixed the CI build error because of unused variable in exception case

* Updating the arlo code based on comment from @balloob. Changed the arm and disarm to enable_motion_detection and disable_motion_detection respectively. Similarly fixed the AttributeError handling. Added dummy code to the demo camera also. Moved out the definitions in const.py into the camera __init__ file

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by houndci-bot

* Fixed the comments posted by travis-ci integration bot

* Fixed the comments posted by travis-ci integration bot

* Fixed the comments posted by travis-ci integration bot for demo.py: expected 2 lines, found 1

* Updated code in camera __init__.py to use the get function instead of directly calling the member in the structure.

* Updated code in camera __init__.py

* Posting the updated code for PR based on @balloob's suggestions/recommendations

* Removed the arlo reference from demo code. Copy-paste error

* Removed the unused import found by hound bot

* Expected 2 lines before function, but found only 1.

* Based on @balloob's comments, moved these constants to the camera/arlo.py

* Added test_demo.py to test the motion enabled and motion disabled in camera component

* Fixing issues found by houndci-bot

* Fixing issues found by houndci-bot

* Fixing the code as per @balloob's suggestions

* Fixing the code as per @balloob's suggestions

* Fixing the test_demo failure. Tried to rewrite a base function to enable the motion in __init__.py and missed to add it to as a job.

* Fixing the hound bot comment

* Update arlo.py

* Update arlo.py
This commit is contained in:
viswa-swami 2017-07-01 00:06:56 -04:00 committed by Paulus Schoutsen
parent 0bcb7839fb
commit ed20f7e359
5 changed files with 186 additions and 7 deletions

View file

@ -12,13 +12,16 @@ from datetime import timedelta
import logging import logging
import hashlib import hashlib
from random import SystemRandom from random import SystemRandom
import os
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import voluptuous as vol
from homeassistant.core import callback 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.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity 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.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SERVICE_EN_MOTION = 'enable_motion_detection'
SERVICE_DISEN_MOTION = 'disable_motion_detection'
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
@ -44,6 +50,24 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom() _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 @asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10): 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()) hass.async_add_job(entity.async_update_ha_state())
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL) 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 return True
@ -126,6 +188,11 @@ class Camera(Entity):
"""Return the camera brand.""" """Return the camera brand."""
return None return None
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return None
@property @property
def model(self): def model(self):
"""Return the camera model.""" """Return the camera model."""
@ -202,6 +269,22 @@ class Camera(Entity):
else: else:
return STATE_IDLE 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 @property
def state_attributes(self): def state_attributes(self):
"""Return the camera state attributes.""" """Return the camera state attributes."""
@ -215,6 +298,9 @@ class Camera(Entity):
if self.brand: if self.brand:
attr['brand'] = self.brand attr['brand'] = self.brand
if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled
return attr return attr
@callback @callback

View file

@ -9,17 +9,19 @@ import logging
import voluptuous as vol 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.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG 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'] DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
@ -46,9 +48,10 @@ class ArloCam(Camera):
def __init__(self, hass, camera, device_info): def __init__(self, hass, camera, device_info):
"""Initialize an Arlo camera.""" """Initialize an Arlo camera."""
super().__init__() super().__init__()
self._camera = camera self._camera = camera
self._base_stn = hass.data['arlo'].base_stations[0]
self._name = self._camera.name self._name = self._camera.name
self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
@ -87,3 +90,27 @@ class ArloCam(Camera):
def brand(self): def brand(self):
"""Camera brand.""" """Camera brand."""
return DEFAULT_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)

View file

@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import os import os
import logging
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo camera platform.""" """Set up the Demo camera platform."""
add_devices([ add_devices([
DemoCamera('Demo camera') DemoCamera(hass, config, 'Demo camera')
]) ])
class DemoCamera(Camera): class DemoCamera(Camera):
"""The representation of a Demo camera.""" """The representation of a Demo camera."""
def __init__(self, name): def __init__(self, hass, config, name):
"""Initialize demo camera component.""" """Initialize demo camera component."""
super().__init__() super().__init__()
self._parent = hass
self._name = name self._name = name
self._motion_status = False
def camera_image(self): def camera_image(self):
"""Return a faked still image response.""" """Return a faked still image response."""
@ -38,3 +42,21 @@ class DemoCamera(Camera):
def name(self): def name(self):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name 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

View file

@ -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'

View file

@ -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')