Skip to content

Added Push Camera#15151

Merged
pvizeli merged 21 commits intohome-assistant:devfrom
dgomes:push.camera
Jul 4, 2018
Merged

Added Push Camera#15151
pvizeli merged 21 commits intohome-assistant:devfrom
dgomes:push.camera

Conversation

@dgomes
Copy link
Copy Markdown
Contributor

@dgomes dgomes commented Jun 25, 2018

Description:

Push Camera is a meta-platform that can receive images through the http API.

A use case, is the integration with motioneye. Motioneye is usually configured to save/record files ONLY when motion is detected. It provides a hook to run a command whenever an image is saved, which can be used together with cURL to send the motion detected images to Push Camera, as showed in this example:

curl -X POST -F "image=@%f" http://hassio.test:8123/api/camera_push/camera.my_push_camera

Optionally the Push Camera is able to buffer a given number of images, creating an animation of the detected motion.

Images are cleared on new events, and events are separated by a soft (configurable) timeout.

Related issue (if applicable): fixes #

Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#5606

Example entry for configuration.yaml (if applicable):

camera:
  - platform: push
    buffer: 3
    timeout: 5

Checklist:

  • The code change is tested and works locally.
  • Local tests pass with tox. Your PR cannot be merged unless tests pass

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • New dependencies have been added to the REQUIREMENTS variable (example).
  • New dependencies are only imported inside functions that use them (example).
  • New or updated dependencies have been added to requirements_all.txt by running script/gen_requirements_all.py.
  • New files were added to .coveragerc.

If the code does not interact with devices:

  • Tests have been added to verify that the new code works.

@awarecan
Copy link
Copy Markdown
Contributor

I have been told not cache image in camera,

Maybe better use await request.multipart() if possible, or at least support that.

Better make 'image' in data['image'].filename configurable.

@frenck
Copy link
Copy Markdown
Member

frenck commented Jun 26, 2018

Little notice: Seems like the docs are missing.

@dgomes dgomes changed the title Added Push Camera WIP: Added Push Camera Jun 26, 2018
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.

Copy link
Copy Markdown
Contributor Author

@dgomes dgomes Jun 26, 2018

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.

Comment thread homeassistant/components/camera/push.py Outdated
self._expired = async_track_point_in_utc_time(
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.

Comment thread homeassistant/components/camera/push.py Outdated

self.schedule_update_ha_state()

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.

Comment thread homeassistant/components/camera/push.py Outdated
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

Comment thread homeassistant/components/camera/push.py Outdated
@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.

Comment thread homeassistant/components/camera/push.py Outdated
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.

Comment thread homeassistant/components/camera/push.py Outdated
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.

@dgomes
Copy link
Copy Markdown
Contributor Author

dgomes commented Jun 26, 2018

@awarecan tks for your comment, per default there is no cache only the current image. But for the described use case (motioneye) makes much sense to cache the event. So lets see what others think about it.

Good point on making image configurable, I will change that later tonight.

Comment thread homeassistant/components/camera/push.py Outdated

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

@dgomes dgomes changed the title WIP: Added Push Camera Added Push Camera Jun 26, 2018
Comment thread tests/components/camera/test_push.py Outdated
resp = await client.post('/api/camera_push/camera.wrong', data=files)
assert resp.status == 400

@mock.patch("PIL.Image.new")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

Comment thread tests/components/camera/test_push.py Outdated
from homeassistant.util import dt as dt_util
from tests.components.auth import async_setup_auth

@mock.patch("PIL.Image.new")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

expected 2 blank lines, found 1

Comment thread homeassistant/components/camera/push.py Outdated
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('Parameter {} missing'.format(self._image), HTTP_BAD_REQUEST)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

line too long (98 > 79 characters)

Comment thread homeassistant/components/camera/push.py Outdated
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('Parameter {} missing'.format(self._image),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

line too long (80 > 79 characters)

Copy link
Copy Markdown
Member

@pvizeli pvizeli left a comment

Choose a reason for hiding this comment

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

Looks good. But we should work like the other cameras and if there is no picutre, we return none.

@dgomes
Copy link
Copy Markdown
Contributor Author

dgomes commented Jun 27, 2018

Returning none will impact the frontend, if the picture takes to long to appear the browser shows a missing image and not the proposed blank (more user friendly)

Either this logic (create a blank image) moves to the parent class, or the frontend should better handle the None image. Opinions ?

@balloob
Copy link
Copy Markdown
Member

balloob commented Jun 27, 2018

Frontend should handle it better, period. No error handling in platforms for things related to the frontend.

Comment thread homeassistant/components/camera/push.py Outdated

self._state = STATE_RECORDING
self._filename = filename
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.

Copy link
Copy Markdown
Contributor Author

@dgomes dgomes Jun 27, 2018

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?

@dgomes
Copy link
Copy Markdown
Contributor Author

dgomes commented Jun 27, 2018

Camera Push will now return image None before first image is received.

Comment thread homeassistant/components/camera/push.py Outdated
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

Comment thread homeassistant/components/camera/push.py Outdated
"""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

self._last_trip = dt_util.utcnow()
self.queue.clear()

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.

Copy link
Copy Markdown
Member

@pvizeli pvizeli Jul 2, 2018

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

@pvizeli pvizeli self-assigned this Jul 1, 2018

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.

@pvizeli pvizeli merged commit 5f7ac09 into home-assistant:dev Jul 4, 2018
@ghost ghost removed the Ready for review label Jul 4, 2018
@mezz64
Copy link
Copy Markdown
Contributor

mezz64 commented Jul 6, 2018

I know this has already been merged, but it presently doesn't seem to actually allow pushing images to multiple defined entities, only the first one configured. All others throw a push camera not found error. The camera card and associated entities are properly created for all defined cameras you just can't post data to any one but the first.

@dgomes
Copy link
Copy Markdown
Contributor Author

dgomes commented Jul 6, 2018

@mezz64 Tks for the bug report, but this is not the proper place :)

I'm already issuing a new PR with a fix!

@dgomes dgomes mentioned this pull request Jul 6, 2018
8 tasks
@home-assistant home-assistant locked as resolved and limited conversation to collaborators Jul 7, 2018
@dgomes dgomes deleted the push.camera branch March 11, 2022 22:28
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants