195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
"""
|
|
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 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
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEPENDENCIES = ['http']
|
|
|
|
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'
|
|
|
|
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,
|
|
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
|
|
})
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities,
|
|
discovery_info=None):
|
|
"""Set up the Push Camera platform."""
|
|
if PUSH_CAMERA_DATA not in hass.data:
|
|
hass.data[PUSH_CAMERA_DATA] = {}
|
|
|
|
cameras = [PushCamera(config[CONF_NAME],
|
|
config[CONF_BUFFER_SIZE],
|
|
config[CONF_TIMEOUT],
|
|
config.get(CONF_TOKEN))]
|
|
|
|
hass.http.register_view(CameraPushReceiver(hass,
|
|
config[CONF_IMAGE_FIELD]))
|
|
|
|
async_add_entities(cameras)
|
|
|
|
|
|
class CameraPushReceiver(HomeAssistantView):
|
|
"""Handle pushes from remote camera."""
|
|
|
|
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."""
|
|
self._cameras = hass.data[PUSH_CAMERA_DATA]
|
|
self._image = image_field
|
|
|
|
async def post(self, request, entity_id):
|
|
"""Accept the POST from Camera."""
|
|
_camera = self._cameras.get(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),
|
|
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()
|
|
_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, token):
|
|
"""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
|
|
self.token = token
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Call when entity is added to hass."""
|
|
self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return 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),
|
|
(ATTR_TOKEN, self.token),
|
|
) if value is not None
|
|
}
|