Async clientsession / fix stuff on aiohttp and camera platform (#4084)
* add websession * convert to websession * convert camera to async * fix lint * fix spell * add import * create task to loop * fix test * update aiohttp * fix tests part 2 * Update aiohttp.py
This commit is contained in:
parent
85747fe2ef
commit
3324995e70
5 changed files with 199 additions and 121 deletions
|
@ -27,8 +27,9 @@ STATE_IDLE = 'idle'
|
|||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
def async_setup(hass, config):
|
||||
"""Setup the camera component."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
@ -36,8 +37,7 @@ def setup(hass, config):
|
|||
hass.http.register_view(CameraImageView(hass, component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -73,7 +73,6 @@ class GenericCamera(Camera):
|
|||
|
||||
self._last_url = None
|
||||
self._last_image = None
|
||||
self._session = aiohttp.ClientSession(loop=hass.loop, auth=self._auth)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
|
@ -111,7 +110,10 @@ class GenericCamera(Camera):
|
|||
else:
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
respone = yield from self._session.get(url)
|
||||
respone = yield from self.hass.websession.get(
|
||||
url,
|
||||
auth=self._auth
|
||||
)
|
||||
self._last_image = yield from respone.read()
|
||||
self.hass.loop.create_task(respone.release())
|
||||
except asyncio.TimeoutError:
|
||||
|
|
|
@ -71,11 +71,11 @@ class MjpegCamera(Camera):
|
|||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||
|
||||
auth = None
|
||||
self._auth = None
|
||||
if self._authentication == HTTP_BASIC_AUTHENTICATION:
|
||||
auth = aiohttp.BasicAuth(self._username, password=self._password)
|
||||
|
||||
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth)
|
||||
self._auth = aiohttp.BasicAuth(
|
||||
self._username, password=self._password
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
@ -103,7 +103,10 @@ class MjpegCamera(Camera):
|
|||
# connect to stream
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from self._session.get(self._mjpeg_url)
|
||||
stream = yield from self.hass.websession.get(
|
||||
self._mjpeg_url,
|
||||
auth=self._auth
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
|
|
|
@ -4,11 +4,14 @@ Support for Synology Surveillance Station Cameras.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import requests
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
|
@ -16,6 +19,7 @@ from homeassistant.const import (
|
|||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -38,6 +42,7 @@ WEBAPI_PATH = '/webapi/'
|
|||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
|
@ -51,77 +56,126 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
QUERY_CGI)
|
||||
query_payload = {'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'}
|
||||
query_req = requests.get(syno_api_url,
|
||||
params=query_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT)
|
||||
query_resp = query_req.json()
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||
|
||||
query_payload = {
|
||||
'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
query_req = yield from hass.websession.get(
|
||||
syno_api_url,
|
||||
params=query_payload,
|
||||
verify=config.get(CONF_VALID_CERT)
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
# cleanup
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
auth_path)
|
||||
session_id = get_session_id(config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
config.get(CONF_VALID_CERT))
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||
|
||||
session_id = yield from get_session_id(
|
||||
hass,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
config.get(CONF_VALID_CERT)
|
||||
)
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
camera_api)
|
||||
camera_payload = {'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'}
|
||||
camera_req = requests.get(syno_camera_url,
|
||||
params=camera_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': session_id})
|
||||
camera_resp = camera_req.json()
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||
|
||||
camera_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
camera_req = yield from hass.websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload,
|
||||
verify_ssl=config.get(CONF_VALID_CERT),
|
||||
cookies={'id': session_id}
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout on %s", syno_camera_url)
|
||||
return False
|
||||
|
||||
camera_resp = yield from camera_req.json()
|
||||
cameras = camera_resp['data']['cameras']
|
||||
yield from camera_req.release()
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
tasks = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
add_devices([SynologyCamera(config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path)])
|
||||
device = SynologyCamera(
|
||||
config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
)
|
||||
tasks.append(device.async_read_sid())
|
||||
devices.append(device)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
hass.loop.create_task(async_add_devices(devices))
|
||||
|
||||
|
||||
def get_session_id(username, password, login_url, valid_cert):
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, username, password, login_url, valid_cert):
|
||||
"""Get a session id."""
|
||||
auth_payload = {'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'}
|
||||
auth_req = requests.get(login_url,
|
||||
params=auth_payload,
|
||||
verify=valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
auth_req = yield from hass.websession.get(
|
||||
login_url,
|
||||
params=auth_payload,
|
||||
verify_ssl=valid_cert
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout on %s", login_url)
|
||||
return False
|
||||
|
||||
auth_resp = yield from auth_req.json()
|
||||
yield from auth_req.release()
|
||||
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
|
||||
|
@ -148,74 +202,92 @@ class SynologyCamera(Camera):
|
|||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._session_id = None
|
||||
|
||||
self._session_id = get_session_id(self._username,
|
||||
self._password,
|
||||
self._login_url,
|
||||
self._valid_cert)
|
||||
|
||||
def get_sid(self):
|
||||
@asyncio.coroutine
|
||||
def async_read_sid(self):
|
||||
"""Get a session id."""
|
||||
auth_payload = {'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': self._username,
|
||||
'passwd': self._password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'}
|
||||
auth_req = requests.get(self._login_url,
|
||||
params=auth_payload,
|
||||
verify=self._valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
self._session_id = auth_resp['data']['sid']
|
||||
self._session_id = yield from get_session_id(
|
||||
self.hass,
|
||||
self._username,
|
||||
self._password,
|
||||
self._login_url,
|
||||
self._valid_cert
|
||||
)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
self.async_camera_image(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(self._synology_url,
|
||||
WEBAPI_PATH,
|
||||
self._camera_path)
|
||||
image_payload = {'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id}
|
||||
image_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||
|
||||
image_payload = {
|
||||
'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
response = requests.get(image_url,
|
||||
params=image_payload,
|
||||
timeout=TIMEOUT,
|
||||
verify=self._valid_cert,
|
||||
cookies={'id': self._session_id})
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from self.hass.websession.get(
|
||||
image_url,
|
||||
params=image_payload,
|
||||
verify_ssl=self._valid_cert,
|
||||
cookies={'id': self._session_id}
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout on %s", image_url)
|
||||
return None
|
||||
|
||||
return response.content
|
||||
image = yield from response.read()
|
||||
yield from response.release()
|
||||
|
||||
def camera_stream(self):
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(self._synology_url,
|
||||
WEBAPI_PATH,
|
||||
self._streaming_path)
|
||||
streaming_payload = {'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'}
|
||||
response = requests.get(streaming_url,
|
||||
payload=streaming_payload,
|
||||
stream=True,
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': self._session_id})
|
||||
return response
|
||||
streaming_url = SYNO_API_URL.format(
|
||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||
|
||||
def mjpeg_steam(self, response):
|
||||
"""Generate an HTTP MJPEG Stream from the Synology NAS."""
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers['CONTENT_TYPE_HEADER'],
|
||||
direct_passthrough=True
|
||||
)
|
||||
streaming_payload = {
|
||||
'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
stream = yield from self.hass.websession.get(
|
||||
streaming_url,
|
||||
payload=streaming_payload,
|
||||
verify_ssl=self._valid_cert,
|
||||
cookies={'id': self._session_id}
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
response.enable_chunked_encoding()
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.release())
|
||||
self.hass.loop.create_task(response.write_eof())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -17,9 +17,9 @@ import threading
|
|||
import time
|
||||
|
||||
from types import MappingProxyType
|
||||
|
||||
from typing import Optional, Any, Callable, List # NOQA
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
|
@ -143,6 +143,7 @@ class HomeAssistant(object):
|
|||
self.config = Config() # type: Config
|
||||
self.state = CoreState.not_running
|
||||
self.exit_code = None
|
||||
self.websession = aiohttp.ClientSession(loop=self.loop)
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue