New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add camera proxy #12006
Add camera proxy #12006
Conversation
f2588c1
to
45a8914
Compare
Maybe |
I don't really think that give the proper connotation. How about 'transform' or 'munge'? While the current code can only do resizing/quality adjustments, I was planning to add stuff like flipping and rotating for the cases where the camera doesn't support those features. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! Good work submit this component 👍
I'm not an official reviewer, but an interested party in this component :)
Just giving you some feedback to get your PR approved ASAP:
You should check voluptuous documentation, default parameters would greatly improve this component.
# Read in HTTP headers: | ||
stream = req.content | ||
# multipart/x-mixed-replace; boundary=--frameboundary | ||
__mimetype, options = cgi.parse_header(req.headers['content-type']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the double underscore ?
Double underscore is used to mangle the attribute names of a class to avoid conflicts of attribute names between classes. That see this as the case.
Just leave let it be a "_" (don't care)
__mimetype, options = cgi.parse_header(req.headers['content-type']) | ||
boundary = options.get('boundary').encode('utf-8') | ||
if not boundary: | ||
raise Exception("Can't find content-type") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would advise a _LOGGER.error() message before the raise. So it can become easier to debug
old_size = len(image) | ||
if old_width <= new_width: | ||
if opts.quality is None: | ||
_LOGGER.debug("Image is smaller than requested width") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
according to your code, it can also be the same size...
newimage = imgbuf.getvalue() | ||
if not opts.force_resize and len(newimage) >= old_size: | ||
_LOGGER.debug("Using original image(%d bytes) " | ||
"because resized image (%d bytes) is larger", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or equal...
self._proxied_camera = config.get(CONF_ENTITY_ID) | ||
self._name = config.get(CONF_NAME) | ||
if not self._name: | ||
self._name = "{} - {}".format(DEFAULT_BASENAME, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes no sense... just pass along this string through the default parameter of the vol.Optional() method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not believe that voluptuous can evaluate a default parameter based on another parameter. The point here is that the camera name needs to include the original proxied name unless the user overrode it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
Still, some room for improvement:
self._name = config.get(CONF_NAME) or "{} - {}".format(DEFAULT_BASENAME, self._proxied_camera)
4 lines can be resumed in this short one as long as config.get(CONF_NAME) is None by default
self.force_resize = force_resize | ||
|
||
def __bool__(self): | ||
"""Bool evaution rules.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
evaluation
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE) | ||
self._cache_images = ( | ||
True | ||
if (config.get(CONF_IMAGE_REFRESH_RATE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does CONF_IMAGE_REFRESH_RATE trigger cache ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the frontend makes a request at an arbitrary rate so you must cache the results when the refresh rate is specified (strictly speaking only when slower than the frontend rate, but there is no way to know what that is)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multilines if statements are unreadable. Also, in this case you just want True or False, so just convert the if-condition to a bool.
As far as further default values in voluptous, I don't see any places where it will help. The only defualt parameter that would make any sense would be image quality, but I want to distinguish between a user specifying a value vs not, and using a default value would mean I just need a different set of logic to get the same result. |
7387138
to
cc17067
Compare
"""Set up the Proxy camera platform.""" | ||
async_add_devices([ProxyCamera(hass, config)]) | ||
|
||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Platforms have no return value
|
||
|
||
@asyncio.coroutine | ||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've switched to async/await syntax so you can drop @asyncio.coroutine
and make it async def async_setup_platform(…)
|
||
|
||
@asyncio.coroutine | ||
def _read_frame(req): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have extract_image_from_jpeg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That function is synchronous. I don't know how to feed async data to it. Is there another way to feed asynchronous data to that function? Also, I don't think this function is suitable for my needs. It reads data by chunks, and if I understand it correctly, it will throw away unused data. I want to capture every image in an mjpeg into an image without dropping any.
if not opts: | ||
return image | ||
|
||
quality = opts.quality if opts.quality else DEFAULT_QUALITY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quality = opts.quality or DEFAULT_QUALITY
|
||
def __bool__(self): | ||
"""Bool evalution rules.""" | ||
return True if self.max_width or self.quality else False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return bool(self.max_width or self.quality)
return newimage | ||
|
||
|
||
class ImageOpts(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can optionally use the attrs
package which is installed as part of Home Assistant core
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless you are going to mandate it, I'd rather not. As far as I can tell, it won't reduce any code for my use case, and would make the code harder to read.
* Add camera proxy * Fix additional tox linting issues * Trivial cleanup * update to new async/await methods rather than decorators. Other minor fixes from code review
Description:
As per discussion in #11577, this is an implementation of a camera proxy. Its primary purpose is to reduce the bandwidth utilization of streaming images to the frontend.
Note that this does not implement dynamic sizing as requested by the frontend as that part of #11577 was more controversial. I may try to implement that capability in a separate patch in the future.
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
tox
run successfully. Your PR cannot be merged unless tests passREQUIREMENTS
variable ([example][ex-requir]).requirements_all.txt
by runningscript/gen_requirements_all.py
..coveragerc
.If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass