Added Push Camera (#15151)

* Added push camera

* add camera.push

* Address comments and add tests

* auff auff

* trip time made no sense

* travis lint

* Mock dependency

* hound

* long line

* long line

* better mocking

* remove blank image

* no more need to mock dependency

* remove import

* cleanup

* no longer needed

* unused constant

* address @pvizeli review

* add force_update

* Revert "add force_update"

This reverts commit e203785ea8.

* rename parameter
This commit is contained in:
Diogo Gomes 2018-07-04 06:44:47 +01:00 committed by Pascal Vizeli
parent 42775142f8
commit 5f7ac09a74
2 changed files with 225 additions and 0 deletions

View file

@ -0,0 +1,162 @@
"""
Camera platform that receives images through HTTP POST.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.push/
"""
import logging
from collections import deque
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
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=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[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
hass.http.register_view(CameraPushReceiver(cameras,
config[CONF_IMAGE_FIELD]))
async_add_devices(cameras)
class CameraPushReceiver(HomeAssistantView):
"""Handle pushes from remote camera."""
url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
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."""
try:
(_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[self._image])
await _camera.update_image(data[self._image].file.read(),
data[self._image].filename)
except ValueError as value_error:
_LOGGER.error("Unknown value %s", value_error)
return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
except KeyError as key_error:
_LOGGER.error('In your POST message %s', key_error)
return self.json_message('{} missing'.format(self._image),
HTTP_BAD_REQUEST)
class PushCamera(Camera):
"""The representation of a Push camera."""
def __init__(self, name, buffer_size, timeout):
"""Initialize push camera component."""
super().__init__()
self._name = name
self._last_trip = None
self._filename = None
self._expired_listener = None
self._state = STATE_IDLE
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
@property
def state(self):
"""Current state of the camera."""
return self._state
async def update_image(self, image, filename):
"""Update the camera image."""
if self._state == STATE_IDLE:
self._state = STATE_RECORDING
self._last_trip = dt_util.utcnow()
self.queue.clear()
self._filename = filename
self.queue.appendleft(image)
@callback
def reset_state(now):
"""Set state to idle after no new images for a period of time."""
self._state = STATE_IDLE
self._expired_listener = None
_LOGGER.debug("Reset state")
self.async_schedule_update_ha_state()
if self._expired_listener:
self._expired_listener()
self._expired_listener = async_track_point_in_utc_time(
self.hass, reset_state, dt_util.utcnow() + self._timeout)
self.async_schedule_update_ha_state()
async def async_camera_image(self):
"""Return a still image response."""
if self.queue:
if self._state == STATE_IDLE:
self.queue.rotate(1)
self._current_image = self.queue[0]
return self._current_image
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
) if value is not None
}

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'