Address comments and add tests

This commit is contained in:
Diogo Gomes
2018-06-26 23:44:32 +01:00
parent 7a7b1a8967
commit 0cbf418f01
2 changed files with 92 additions and 27 deletions

View File

@@ -5,9 +5,9 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.push/ https://home-assistant.io/components/camera.push/
""" """
import logging import logging
import datetime
from collections import deque from collections import deque
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
@@ -23,33 +23,35 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
API_URL = "/api/camera_push/{entity_id}" CONF_BUFFER_SIZE = 'cache'
CONF_IMAGE_FIELD = 'image'
CONF_BUFFER_SIZE = "cache" DEFAULT_NAME = "Push Camera"
DEFAULT_NAME = 'Push Camera'
BLANK_IMAGE_SIZE = (640, 480) BLANK_IMAGE_SIZE = (640, 480)
ATTR_FILENAME = "filename" ATTR_FILENAME = 'filename'
REQUIREMENTS = ['pillow==5.0.0'] REQUIREMENTS = ['pillow==5.0.0']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int, vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int,
vol.Optional(CONF_TIMEOUT, default=5): cv.positive_int, vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
}) })
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None): discovery_info=None):
"""Set up the Push Camera platform.""" """Set up the Push Camera platform."""
cameras = [PushCamera(config.get(CONF_NAME), cameras = [PushCamera(config[CONF_NAME],
config.get(CONF_BUFFER_SIZE), config[CONF_BUFFER_SIZE],
config.get(CONF_TIMEOUT))] config[CONF_TIMEOUT])]
hass.http.register_view(CameraPushReceiver(cameras)) hass.http.register_view(CameraPushReceiver(cameras,
config[CONF_IMAGE_FIELD]))
async_add_devices(cameras) async_add_devices(cameras)
@@ -57,12 +59,13 @@ async def async_setup_platform(hass, config, async_add_devices,
class CameraPushReceiver(HomeAssistantView): class CameraPushReceiver(HomeAssistantView):
"""Handle pushes from remote camera.""" """Handle pushes from remote camera."""
url = API_URL url = "/api/camera_push/{entity_id}"
name = 'api:camera:push' name = 'api:camera_push:camera_entity'
def __init__(self, cameras): def __init__(self, cameras, image_field):
"""Initialize CameraPushReceiver with camera entity.""" """Initialize CameraPushReceiver with camera entity."""
self._cameras = cameras self._cameras = cameras
self._image = image_field
async def post(self, request, entity_id): async def post(self, request, entity_id):
"""Accept the POST from Camera.""" """Accept the POST from Camera."""
@@ -70,16 +73,21 @@ class CameraPushReceiver(HomeAssistantView):
(_camera,) = [camera for camera in self._cameras (_camera,) = [camera for camera in self._cameras
if camera.entity_id == entity_id] if camera.entity_id == entity_id]
except ValueError: except ValueError:
_LOGGER.error("Unknown push camera %s", entity_id)
return self.json_message('Unknown Push Camera', return self.json_message('Unknown Push Camera',
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
try: try:
data = await request.post() data = await request.post()
_LOGGER.debug("Received Camera push: %s", data['image']) _LOGGER.debug("Received Camera push: %s", data[self._image])
await _camera.update_image(data['image'].file.read(), await _camera.update_image(data[self._image].file.read(),
data['image'].filename) data[self._image].filename)
except ValueError: except ValueError as v:
_LOGGER.error("Unknown value %s", v)
return self.json_message('Invalid POST', HTTP_BAD_REQUEST) return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
except KeyError as k:
_LOGGER.error('In your POST message %s', k)
return self.json_message('Parameter %s missing', HTTP_BAD_REQUEST)
class PushCamera(Camera): class PushCamera(Camera):
@@ -89,12 +97,11 @@ class PushCamera(Camera):
"""Initialize push camera component.""" """Initialize push camera component."""
super().__init__() super().__init__()
self._name = name self._name = name
self._motion_status = False
self._last_trip = None self._last_trip = None
self._filename = None self._filename = None
self._expired = None self._expired = None
self._state = STATE_IDLE self._state = STATE_IDLE
self._timeout = datetime.timedelta(seconds=timeout) self._timeout = timeout
self.queue = deque([], buffer_size) self.queue = deque([], buffer_size)
from PIL import Image from PIL import Image
@@ -137,12 +144,7 @@ class PushCamera(Camera):
self._expired = async_track_point_in_utc_time( self._expired = async_track_point_in_utc_time(
self.hass, reset_state, dt_util.utcnow() + self._timeout) self.hass, reset_state, dt_util.utcnow() + self._timeout)
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
def camera_image(self):
"""Return a still image response."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
async def async_camera_image(self): async def async_camera_image(self):
"""Return a still image response.""" """Return a still image response."""
@@ -161,7 +163,7 @@ class PushCamera(Camera):
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
"""Camera Motion Detection Status.""" """Camera Motion Detection Status."""
return self._motion_status return False
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@@ -0,0 +1,63 @@
"""The tests for generic camera component."""
import io
from datetime import timedelta
from homeassistant import core as ha
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.components.auth import async_setup_auth
async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
"""Test that posting to wrong api endpoint fails."""
await async_setup_component(hass, 'camera', {
'camera': {
'platform': 'push',
'name': 'config_test',
}})
client = await async_setup_auth(hass, aiohttp_client)
# missing file
resp = await client.post('/api/camera_push/camera.config_test')
assert resp.status == 400
files = {'image': io.BytesIO(b'fake')}
# wrong entity
resp = await client.post('/api/camera_push/camera.wrong', data=files)
assert resp.status == 400
async def test_posting_url(aioclient_mock, hass, aiohttp_client):
"""Test that posting to api endpoint works."""
await async_setup_component(hass, 'camera', {
'camera': {
'platform': 'push',
'name': 'config_test',
}})
client = await async_setup_auth(hass, aiohttp_client)
files = {'image': io.BytesIO(b'fake')}
# initial state
camera_state = hass.states.get('camera.config_test')
assert camera_state.state == 'idle'
# post image
resp = await client.post('/api/camera_push/camera.config_test', data=files)
assert resp.status == 200
# state recording
camera_state = hass.states.get('camera.config_test')
assert camera_state.state == 'recording'
# await timeout
shifted_time = dt_util.utcnow() + timedelta(seconds=15)
hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time})
await hass.async_block_till_done()
# back to initial state
camera_state = hass.states.get('camera.config_test')
assert camera_state.state == 'idle'