Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
174 changes: 174 additions & 0 deletions homeassistant/components/camera/push.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
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
import datetime

from collections import deque
import voluptuous as vol

from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING
from homeassistant.util.async_ import run_coroutine_threadsafe

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'homeassistant.util.async_.run_coroutine_threadsafe' imported but unused

from homeassistant.core import callback
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, HTTP_BAD_REQUEST,\
ATTR_LAST_TRIP_TIME
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__)

API_URL = "/api/camera_push/{entity_id}"

CONF_BUFFER_SIZE = "cache"

DEFAULT_NAME = 'Push Camera'

BLANK_IMAGE_SIZE = (640, 480)

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,

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.

maybe queue_size ? I'm not sure if buffer_size to odd

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.

It's a buffer... that happens to be implemented with a queue.

I don't think it makes sense to rename it.

vol.Optional(CONF_TIMEOUT, default=5): cv.positive_int,
})


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),

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.

dict.get isn't needed since there are defaults in place via the config schema.

config.get(CONF_BUFFER_SIZE),
config.get(CONF_TIMEOUT))]

hass.http.register_view(CameraPushReceiver(cameras))

async_add_devices(cameras)


class CameraPushReceiver(HomeAssistantView):
"""Handle pushes from remote camera."""

url = API_URL
name = 'api:camera:push'

def __init__(self, cameras):
"""Initialize CameraPushReceiver with camera entity."""
self._cameras = cameras

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:
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(),

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.

Is this api to POST an image standardized or is it specific for this implementation?

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.

It is not standardised, I will make it configurable as suggested by @awarecan

data['image'].filename)
except ValueError:
return self.json_message('Invalid POST', 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._motion_status = False
self._last_trip = None
self._filename = None
self._expired = None
self._state = STATE_IDLE
self._timeout = datetime.timedelta(seconds=timeout)

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.

Use the timedelta config validation helper in the schema instead.

self.queue = deque([], buffer_size)

from PIL import Image
import io

image = Image.new('RGB', BLANK_IMAGE_SIZE)

imgbuf = io.BytesIO()
image.save(imgbuf, "JPEG")

self.queue.append(imgbuf.getvalue())
self._current_image = imgbuf.getvalue()

@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._last_trip = dt_util.utcnow()

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 seems to be the same as last_changed state attribute. If that's true, why do we need this?

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.

It is not, this will refer to the moment we start recording, last_changed will be updated during recording. The duration of the recording will be last_changed - last_trip

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.

If state is the same, ie recording, last_changed will not be updated after a state update.

@dgomes dgomes Jun 26, 2018

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.

last_trip is only set on the transition STATE_IDLE -> STATE_RECORDING
last_changed is set on both transitions

both attributes will be equal only during the STATE_RECORDING state.

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.

Ok, thanks for clarifying.

self.queue.clear()

self._state = STATE_RECORDING
self._filename = filename

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.

do we really need this? That make a state change any new picture is pushed

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.

That is actually a feature :)

That state change will help automations that require a trigger (e.g. do some image_processing, send a notification).

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.

Yeah, that will be trigger if the state of camera going to recording, why you need after that a state change?

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.

In the use case of motioneye, an event will consist of 3 images (default)

I expect to be notified of the 3 for the aforementioned automations.

@pvizeli pvizeli Jul 2, 2018

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.

I have a idea. Add the options force_update (default false) and use this on entity as attribute force_update.
Other platform make the same. That allow user to force state change on if a update is exists. I think it is a bit wired to store a filename into attribute to have the same effect. What do you think?

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.

That is possible yes, I can work that out tonight.

But I also believe the filename can be a useful attribute by itself

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.

For what? Can you give a example why to you need this attribute?

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.

Filename is a unique key, and can be used to trace events.

Personal example:
MotionEye sends HA an image, HA sends a notification with the image. If I want to go back to motionEye image storage, I need a key to look for. Sending the filename with the notification lets me go back to motionEye storage and look for the image and review the security event (way past the time HA has cleared the image from the queue)

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.

Ok. Remove the force_update if you want staying with that attribute and we can merge it.

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.

Done

self.queue.append(image)

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.

I feel like this platform is getting over complicated. It should just show the last pushed image.

We should also store that image on disk and not in memory.

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.

With que, you could see a stream if you push multible images into the entity. Maybe default 1 and you can overwrite this with a option?

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 is abusing a feature of our camera integration. The camera get_image function should show a realtime stream of what a camera is showing.

We don't support replays in our camera integration right now. So we shouldn't hack it in.

@dgomes dgomes Jun 27, 2018

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 feel strongly against writing to disk. Most people run HA on an SD card, it's really bad to write continuously to the SD.

I share @pvizeli comment (that was my intention in the implementation, therefore the current default size of deque = 1).

Let me remind you that the most common use case for this camera is not realtime, and most of the time the user will be faced with the last image (very poor context on what happened).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It could be resolved by allow customized save image path, then point it to some tmpfs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let me remind you that the most common use case for this camera is not realtime

I don't like this assumption. If you think you need a well-tuned components only for your use case, probably should not use a such generic name.

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.

@awarecan sure it can (thats how I did before developing this code)

But that's not easy for the average user, and would require hassio/hassos to be aware of that need.

We are not discussing full length video, we are discussing 3 or 5 images (it doesn't take that much memory)

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.

@awarecan I first had this component living in my custom_components repo (name is http_post). But realised people were using it for the described use case.

There is nothing specific in the component related to motionEye, so I did not call it camera.motionEye and even made some changes to make it even more general.

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.

A Raspberry Pi has 1GB of RAM. If 10 images are kept around, 1MB each, we'll be using 1% of memory for this feature.

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.

@balloob what is now the plan, remove or hold?


@callback
def reset_state(now):
"""Set state to idle after no new images for a period of time."""
self._state = STATE_IDLE
self.async_schedule_update_ha_state()
self._expired = None

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.

first set all variable and after that call the function

_LOGGER.debug("Reset state")

if self._expired:
self._expired()

self._expired = async_track_point_in_utc_time(

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.

rename this like _expired_listener so that you know what it is

self.hass, reset_state, dt_util.utcnow() + self._timeout)

self.schedule_update_ha_state()

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.

Use the async version here since we're in a coroutine.


def camera_image(self):

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 isn't needed since we're overwriting the async version of this method below.

"""Return a still image response."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()

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 self._motion_status

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.

If this is static, we can just return False here and don't need the instance attribute.


@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
name: value for name, value in (
(ATTR_LAST_TRIP_TIME, self._last_trip),
(ATTR_FILENAME, self._filename),
) if value is not None
}
1 change: 1 addition & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ piglow==1.2.4
pilight==0.1.1

# homeassistant.components.camera.proxy
# homeassistant.components.camera.push
pillow==5.0.0

# homeassistant.components.dominos
Expand Down