Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions homeassistant/components/camera/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
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.components.http.view import KEY_AUTHENTICATED,\
HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, 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
Expand All @@ -25,11 +27,13 @@

CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'

DEFAULT_NAME = "Push Camera"

ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'

PUSH_CAMERA_DATA = 'push_camera'

Expand All @@ -39,6 +43,7 @@
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,
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be required

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it optional, since the implementation allows the use of the normal authentication.

})


Expand All @@ -50,7 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities,

cameras = [PushCamera(config[CONF_NAME],
config[CONF_BUFFER_SIZE],
config[CONF_TIMEOUT])]
config[CONF_TIMEOUT],
config.get(CONF_TOKEN))]

hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
Expand All @@ -63,6 +69,7 @@ class CameraPushReceiver(HomeAssistantView):

url = "/api/camera_push/{entity_id}"
name = 'api:camera_push:camera_entity'
requires_auth = False

def __init__(self, hass, image_field):
"""Initialize CameraPushReceiver with camera entity."""
Expand All @@ -75,8 +82,21 @@ async def post(self, request, entity_id):

if _camera is None:
_LOGGER.error("Unknown %s", entity_id)
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
else HTTP_UNAUTHORIZED
return self.json_message('Unknown {}'.format(entity_id),
HTTP_BAD_REQUEST)
status)

# Supports HA authentication and token based
# when token has been configured
authenticated = (request[KEY_AUTHENTICATED] or
(_camera.token is not None and
request.query.get('token') == _camera.token))

if not authenticated:
return self.json_message(
'Invalid authorization credentials for {}'.format(entity_id),
HTTP_UNAUTHORIZED)

try:
data = await request.post()
Expand All @@ -95,7 +115,7 @@ async def post(self, request, entity_id):
class PushCamera(Camera):
"""The representation of a Push camera."""

def __init__(self, name, buffer_size, timeout):
def __init__(self, name, buffer_size, timeout, token):
"""Initialize push camera component."""
super().__init__()
self._name = name
Expand All @@ -106,6 +126,7 @@ def __init__(self, name, buffer_size, timeout):
self._timeout = timeout
self.queue = deque([], buffer_size)
self._current_image = None
self.token = token

async def async_added_to_hass(self):
"""Call when entity is added to hass."""
Expand Down Expand Up @@ -168,5 +189,6 @@ def device_state_attributes(self):
name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None
}
67 changes: 60 additions & 7 deletions tests/components/camera/test_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
from homeassistant.components.http.auth import setup_auth


async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
Expand All @@ -15,19 +15,69 @@ async def test_bad_posting(aioclient_mock, hass, aiohttp_client):
'camera': {
'platform': 'push',
'name': 'config_test',
'token': '12345678'
}})

client = await async_setup_auth(hass, aiohttp_client)
client = await aiohttp_client(hass.http.app)

# missing file
resp = await client.post('/api/camera_push/camera.config_test')
assert resp.status == 400

files = {'image': io.BytesIO(b'fake')}

# wrong entity
files = {'image': io.BytesIO(b'fake')}
resp = await client.post('/api/camera_push/camera.wrong', data=files)
assert resp.status == 400
assert resp.status == 404


async def test_cases_with_no_auth(aioclient_mock, hass, aiohttp_client):
"""Test cases where aiohttp_client is not auth."""
await async_setup_component(hass, 'camera', {
'camera': {
'platform': 'push',
'name': 'config_test',
'token': '12345678'
}})

setup_auth(hass.http.app, [], True, api_password=None)
client = await aiohttp_client(hass.http.app)

# wrong token
files = {'image': io.BytesIO(b'fake')}
resp = await client.post('/api/camera_push/camera.config_test?token=1234',
data=files)
assert resp.status == 401

# right token
files = {'image': io.BytesIO(b'fake')}
resp = await client.post(
'/api/camera_push/camera.config_test?token=12345678',
data=files)
assert resp.status == 200


async def test_no_auth_no_token(aioclient_mock, hass, aiohttp_client):
"""Test cases where aiohttp_client is not auth."""
await async_setup_component(hass, 'camera', {
'camera': {
'platform': 'push',
'name': 'config_test',
}})

setup_auth(hass.http.app, [], True, api_password=None)
client = await aiohttp_client(hass.http.app)

# no token
files = {'image': io.BytesIO(b'fake')}
resp = await client.post('/api/camera_push/camera.config_test',
data=files)
assert resp.status == 401

# fake token
files = {'image': io.BytesIO(b'fake')}
resp = await client.post(
'/api/camera_push/camera.config_test?token=12345678',
data=files)
assert resp.status == 401


async def test_posting_url(hass, aiohttp_client):
Expand All @@ -36,6 +86,7 @@ async def test_posting_url(hass, aiohttp_client):
'camera': {
'platform': 'push',
'name': 'config_test',
'token': '12345678'
}})

client = await aiohttp_client(hass.http.app)
Expand All @@ -46,7 +97,9 @@ async def test_posting_url(hass, aiohttp_client):
assert camera_state.state == 'idle'

# post image
resp = await client.post('/api/camera_push/camera.config_test', data=files)
resp = await client.post(
'/api/camera_push/camera.config_test?token=12345678',
data=files)
assert resp.status == 200

# state recording
Expand Down