mirror of
https://github.com/home-assistant/core.git
synced 2025-08-12 09:05:15 +02:00
Address comments and add tests
This commit is contained in:
@@ -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):
|
||||||
|
63
tests/components/camera/test_push.py
Normal file
63
tests/components/camera/test_push.py
Normal 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'
|
Reference in New Issue
Block a user