Fix camera proxy to not require api_password to function (#16450)
parent
5bd9be6252
commit
cf4b72e00e
|
@ -142,6 +142,68 @@ def async_snapshot(hass, filename, entity_id=None):
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def async_get_image(hass, entity_id, timeout=10):
|
async def async_get_image(hass, entity_id, timeout=10):
|
||||||
"""Fetch an image from a camera entity."""
|
"""Fetch an image from a camera entity."""
|
||||||
|
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||||
|
|
||||||
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||||
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
|
image = await camera.async_camera_image()
|
||||||
|
|
||||||
|
if image:
|
||||||
|
return Image(camera.content_type, image)
|
||||||
|
|
||||||
|
raise HomeAssistantError('Unable to get image')
|
||||||
|
|
||||||
|
|
||||||
|
@bind_hass
|
||||||
|
async def async_get_mjpeg_stream(hass, request, entity_id):
|
||||||
|
"""Fetch an mjpeg stream from a camera entity."""
|
||||||
|
camera = _get_camera_from_entity_id(hass, entity_id)
|
||||||
|
|
||||||
|
return await camera.handle_async_mjpeg_stream(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_still_stream(request, image_cb, content_type, interval):
|
||||||
|
"""Generate an HTTP MJPEG stream from camera images.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
|
response = web.StreamResponse()
|
||||||
|
response.content_type = ('multipart/x-mixed-replace; '
|
||||||
|
'boundary=--frameboundary')
|
||||||
|
await response.prepare(request)
|
||||||
|
|
||||||
|
async def write_to_mjpeg_stream(img_bytes):
|
||||||
|
"""Write image to stream."""
|
||||||
|
await response.write(bytes(
|
||||||
|
'--frameboundary\r\n'
|
||||||
|
'Content-Type: {}\r\n'
|
||||||
|
'Content-Length: {}\r\n\r\n'.format(
|
||||||
|
content_type, len(img_bytes)),
|
||||||
|
'utf-8') + img_bytes + b'\r\n')
|
||||||
|
|
||||||
|
last_image = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
img_bytes = await image_cb()
|
||||||
|
if not img_bytes:
|
||||||
|
break
|
||||||
|
|
||||||
|
if img_bytes != last_image:
|
||||||
|
await write_to_mjpeg_stream(img_bytes)
|
||||||
|
|
||||||
|
# Chrome seems to always ignore first picture,
|
||||||
|
# print it twice.
|
||||||
|
if last_image is None:
|
||||||
|
await write_to_mjpeg_stream(img_bytes)
|
||||||
|
last_image = img_bytes
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_camera_from_entity_id(hass, entity_id):
|
||||||
|
"""Get camera component from entity_id."""
|
||||||
component = hass.data.get(DOMAIN)
|
component = hass.data.get(DOMAIN)
|
||||||
|
|
||||||
if component is None:
|
if component is None:
|
||||||
|
@ -155,14 +217,7 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||||
if not camera.is_on:
|
if not camera.is_on:
|
||||||
raise HomeAssistantError('Camera is off')
|
raise HomeAssistantError('Camera is off')
|
||||||
|
|
||||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
return camera
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
|
||||||
image = await camera.async_camera_image()
|
|
||||||
|
|
||||||
if image:
|
|
||||||
return Image(camera.content_type, image)
|
|
||||||
|
|
||||||
raise HomeAssistantError('Unable to get image')
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
|
@ -290,39 +345,8 @@ class Camera(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
response = web.StreamResponse()
|
return await async_get_still_stream(request, self.async_camera_image,
|
||||||
response.content_type = ('multipart/x-mixed-replace; '
|
self.content_type, interval)
|
||||||
'boundary=--frameboundary')
|
|
||||||
await response.prepare(request)
|
|
||||||
|
|
||||||
async def write_to_mjpeg_stream(img_bytes):
|
|
||||||
"""Write image to stream."""
|
|
||||||
await response.write(bytes(
|
|
||||||
'--frameboundary\r\n'
|
|
||||||
'Content-Type: {}\r\n'
|
|
||||||
'Content-Length: {}\r\n\r\n'.format(
|
|
||||||
self.content_type, len(img_bytes)),
|
|
||||||
'utf-8') + img_bytes + b'\r\n')
|
|
||||||
|
|
||||||
last_image = None
|
|
||||||
|
|
||||||
while True:
|
|
||||||
img_bytes = await self.async_camera_image()
|
|
||||||
if not img_bytes:
|
|
||||||
break
|
|
||||||
|
|
||||||
if img_bytes and img_bytes != last_image:
|
|
||||||
await write_to_mjpeg_stream(img_bytes)
|
|
||||||
|
|
||||||
# Chrome seems to always ignore first picture,
|
|
||||||
# print it twice.
|
|
||||||
if last_image is None:
|
|
||||||
await write_to_mjpeg_stream(img_bytes)
|
|
||||||
last_image = img_bytes
|
|
||||||
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(self, request):
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
"""Serve an HTTP MJPEG stream from the camera.
|
"""Serve an HTTP MJPEG stream from the camera.
|
||||||
|
|
|
@ -7,17 +7,15 @@ https://www.home-assistant.io/components/camera.proxy/
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import async_timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
|
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
|
||||||
async_aiohttp_proxy_web, async_get_clientsession)
|
|
||||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
from . import async_get_still_stream
|
||||||
|
|
||||||
REQUIREMENTS = ['pillow==5.2.0']
|
REQUIREMENTS = ['pillow==5.2.0']
|
||||||
|
|
||||||
|
@ -158,22 +156,14 @@ class ProxyCamera(Camera):
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
||||||
self._last_image_time = now
|
self._last_image_time = now
|
||||||
url = "{}/api/camera_proxy/{}".format(
|
image = await self.hass.components.camera.async_get_image(
|
||||||
self.hass.config.api.base_url, self._proxied_camera)
|
self._proxied_camera)
|
||||||
try:
|
if not image:
|
||||||
websession = async_get_clientsession(self.hass)
|
_LOGGER.error("Error getting original camera image")
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
|
||||||
response = await websession.get(url, headers=self._headers)
|
|
||||||
image = await response.read()
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
_LOGGER.error("Timeout getting camera image")
|
|
||||||
return self._last_image
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
_LOGGER.error("Error getting new camera image: %s", err)
|
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
||||||
image = await self.hass.async_add_job(
|
image = await self.hass.async_add_job(
|
||||||
_resize_image, image, self._image_opts)
|
_resize_image, image.content, self._image_opts)
|
||||||
|
|
||||||
if self._cache_images:
|
if self._cache_images:
|
||||||
self._last_image = image
|
self._last_image = image
|
||||||
|
@ -181,56 +171,28 @@ class ProxyCamera(Camera):
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(self, request):
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
"""Generate an HTTP MJPEG stream from camera images."""
|
"""Generate an HTTP MJPEG stream from camera images."""
|
||||||
websession = async_get_clientsession(self.hass)
|
|
||||||
url = "{}/api/camera_proxy_stream/{}".format(
|
|
||||||
self.hass.config.api.base_url, self._proxied_camera)
|
|
||||||
stream_coro = websession.get(url, headers=self._headers)
|
|
||||||
|
|
||||||
if not self._stream_opts:
|
if not self._stream_opts:
|
||||||
return await async_aiohttp_proxy_web(
|
return await self.hass.components.camera.async_get_mjpeg_stream(
|
||||||
self.hass, request, stream_coro)
|
request, self._proxied_camera)
|
||||||
|
|
||||||
response = aiohttp.web.StreamResponse()
|
return await async_get_still_stream(
|
||||||
response.content_type = (
|
request, self._async_stream_image,
|
||||||
'multipart/x-mixed-replace; boundary=--frameboundary')
|
self.content_type, self.frame_interval)
|
||||||
await response.prepare(request)
|
|
||||||
|
|
||||||
async def write(img_bytes):
|
|
||||||
"""Write image to stream."""
|
|
||||||
await response.write(bytes(
|
|
||||||
'--frameboundary\r\n'
|
|
||||||
'Content-Type: {}\r\n'
|
|
||||||
'Content-Length: {}\r\n\r\n'.format(
|
|
||||||
self.content_type, len(img_bytes)),
|
|
||||||
'utf-8') + img_bytes + b'\r\n')
|
|
||||||
|
|
||||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
|
||||||
req = await stream_coro
|
|
||||||
|
|
||||||
try:
|
|
||||||
# This would be nicer as an async generator
|
|
||||||
# But that would only be supported for python >=3.6
|
|
||||||
data = b''
|
|
||||||
stream = req.content
|
|
||||||
while True:
|
|
||||||
chunk = await stream.read(102400)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
data += chunk
|
|
||||||
jpg_start = data.find(b'\xff\xd8')
|
|
||||||
jpg_end = data.find(b'\xff\xd9')
|
|
||||||
if jpg_start != -1 and jpg_end != -1:
|
|
||||||
image = data[jpg_start:jpg_end + 2]
|
|
||||||
image = await self.hass.async_add_job(
|
|
||||||
_resize_image, image, self._stream_opts)
|
|
||||||
await write(image)
|
|
||||||
data = data[jpg_end + 2:]
|
|
||||||
finally:
|
|
||||||
req.close()
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this camera."""
|
"""Return the name of this camera."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
async def _async_stream_image(self):
|
||||||
|
"""Return a still image response from the camera."""
|
||||||
|
try:
|
||||||
|
image = await self.hass.components.camera.async_get_image(
|
||||||
|
self._proxied_camera)
|
||||||
|
if not image:
|
||||||
|
return None
|
||||||
|
except HomeAssistantError:
|
||||||
|
raise asyncio.CancelledError
|
||||||
|
|
||||||
|
return await self.hass.async_add_job(
|
||||||
|
_resize_image, image.content, self._stream_opts)
|
||||||
|
|
Loading…
Reference in New Issue