From 0cbf418f0158c032b582a7a386f462470016d49f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 26 Jun 2018 23:44:32 +0100 Subject: [PATCH] Address comments and add tests --- homeassistant/components/camera/push.py | 56 +++++++++++----------- tests/components/camera/test_push.py | 63 +++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 tests/components/camera/test_push.py diff --git a/homeassistant/components/camera/push.py b/homeassistant/components/camera/push.py index b34fd5b843e..61b33471de6 100644 --- a/homeassistant/components/camera/push.py +++ b/homeassistant/components/camera/push.py @@ -5,9 +5,9 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/camera.push/ """ import logging -import datetime from collections import deque +from datetime import timedelta import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ @@ -23,33 +23,35 @@ import homeassistant.util.dt as dt_util _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) -ATTR_FILENAME = "filename" +ATTR_FILENAME = 'filename' REQUIREMENTS = ['pillow==5.0.0'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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, discovery_info=None): """Set up the Push Camera platform.""" - cameras = [PushCamera(config.get(CONF_NAME), - config.get(CONF_BUFFER_SIZE), - config.get(CONF_TIMEOUT))] + cameras = [PushCamera(config[CONF_NAME], + config[CONF_BUFFER_SIZE], + config[CONF_TIMEOUT])] - hass.http.register_view(CameraPushReceiver(cameras)) + hass.http.register_view(CameraPushReceiver(cameras, + config[CONF_IMAGE_FIELD])) async_add_devices(cameras) @@ -57,12 +59,13 @@ async def async_setup_platform(hass, config, async_add_devices, class CameraPushReceiver(HomeAssistantView): """Handle pushes from remote camera.""" - url = API_URL - name = 'api:camera:push' + url = "/api/camera_push/{entity_id}" + name = 'api:camera_push:camera_entity' - def __init__(self, cameras): + def __init__(self, cameras, image_field): """Initialize CameraPushReceiver with camera entity.""" self._cameras = cameras + self._image = image_field async def post(self, request, entity_id): """Accept the POST from Camera.""" @@ -70,16 +73,21 @@ class CameraPushReceiver(HomeAssistantView): (_camera,) = [camera for camera in self._cameras if camera.entity_id == entity_id] except ValueError: + _LOGGER.error("Unknown push camera %s", entity_id) return self.json_message('Unknown Push Camera', HTTP_BAD_REQUEST) try: data = await request.post() - _LOGGER.debug("Received Camera push: %s", data['image']) - await _camera.update_image(data['image'].file.read(), - data['image'].filename) - except ValueError: + _LOGGER.debug("Received Camera push: %s", data[self._image]) + await _camera.update_image(data[self._image].file.read(), + data[self._image].filename) + except ValueError as v: + _LOGGER.error("Unknown value %s", v) 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): @@ -89,12 +97,11 @@ class PushCamera(Camera): """Initialize push camera component.""" super().__init__() self._name = name - self._motion_status = False self._last_trip = None self._filename = None self._expired = None self._state = STATE_IDLE - self._timeout = datetime.timedelta(seconds=timeout) + self._timeout = timeout self.queue = deque([], buffer_size) from PIL import Image @@ -137,12 +144,7 @@ class PushCamera(Camera): self._expired = async_track_point_in_utc_time( self.hass, reset_state, dt_util.utcnow() + self._timeout) - self.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() + self.async_schedule_update_ha_state() async def async_camera_image(self): """Return a still image response.""" @@ -161,7 +163,7 @@ class PushCamera(Camera): @property def motion_detection_enabled(self): """Camera Motion Detection Status.""" - return self._motion_status + return False @property def device_state_attributes(self): diff --git a/tests/components/camera/test_push.py b/tests/components/camera/test_push.py new file mode 100644 index 00000000000..78053e540f5 --- /dev/null +++ b/tests/components/camera/test_push.py @@ -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'