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:
Pascal Vizeli 2016-10-28 06:40:10 +02:00 committed by Paulus Schoutsen
parent 85747fe2ef
commit 3324995e70
5 changed files with 199 additions and 121 deletions

View file

@ -27,8 +27,9 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
@asyncio.coroutine
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(hass, config): def async_setup(hass, config):
"""Setup the camera component.""" """Setup the camera component."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) 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(CameraImageView(hass, component.entities))
hass.http.register_view(CameraMjpegStream(hass, component.entities)) hass.http.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config) yield from component.async_setup(config)
return True return True

View file

@ -73,7 +73,6 @@ class GenericCamera(Camera):
self._last_url = None self._last_url = None
self._last_image = None self._last_image = None
self._session = aiohttp.ClientSession(loop=hass.loop, auth=self._auth)
def camera_image(self): def camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
@ -111,7 +110,10 @@ class GenericCamera(Camera):
else: else:
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): 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._last_image = yield from respone.read()
self.hass.loop.create_task(respone.release()) self.hass.loop.create_task(respone.release())
except asyncio.TimeoutError: except asyncio.TimeoutError:

View file

@ -71,11 +71,11 @@ class MjpegCamera(Camera):
self._password = device_info.get(CONF_PASSWORD) self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL] self._mjpeg_url = device_info[CONF_MJPEG_URL]
auth = None self._auth = None
if self._authentication == HTTP_BASIC_AUTHENTICATION: if self._authentication == HTTP_BASIC_AUTHENTICATION:
auth = aiohttp.BasicAuth(self._username, password=self._password) self._auth = aiohttp.BasicAuth(
self._username, password=self._password
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth) )
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -103,7 +103,10 @@ class MjpegCamera(Camera):
# connect to stream # connect to stream
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): 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: except asyncio.TimeoutError:
raise HTTPGatewayTimeout() raise HTTPGatewayTimeout()

View file

@ -4,11 +4,14 @@ Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/ https://home-assistant.io/components/camera.synology/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import requests from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,6 +42,7 @@ WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi' AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi' CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}' 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.""" """Setup a Synology IP Camera."""
# Determine API to use for authentication # Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(config.get(CONF_URL), syno_api_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
QUERY_CGI)
query_payload = {'api': QUERY_API, query_payload = {
'method': 'Query', 'api': QUERY_API,
'version': '1', 'method': 'Query',
'query': 'SYNO.'} 'version': '1',
query_req = requests.get(syno_api_url, 'query': 'SYNO.'
params=query_payload, }
verify=config.get(CONF_VALID_CERT), try:
timeout=TIMEOUT) with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_resp = query_req.json() 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'] auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path'] camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path']
# cleanup
yield from query_req.release()
# Authticate to NAS to get a session id # Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL), syno_auth_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, auth_path)
auth_path)
session_id = get_session_id(config.get(CONF_USERNAME), session_id = yield from get_session_id(
config.get(CONF_PASSWORD), hass,
syno_auth_url, config.get(CONF_USERNAME),
config.get(CONF_VALID_CERT)) config.get(CONF_PASSWORD),
syno_auth_url,
config.get(CONF_VALID_CERT)
)
# Use SessionID to get cameras in system # Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL), syno_camera_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_api)
camera_payload = {'api': CAMERA_API, camera_payload = {
'method': 'List', 'api': CAMERA_API,
'version': '1'} 'method': 'List',
camera_req = requests.get(syno_camera_url, 'version': '1'
params=camera_payload, }
verify=config.get(CONF_VALID_CERT), try:
timeout=TIMEOUT, with async_timeout.timeout(TIMEOUT, loop=hass.loop):
cookies={'id': session_id}) camera_req = yield from hass.websession.get(
camera_resp = camera_req.json() 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'] cameras = camera_resp['data']['cameras']
yield from camera_req.release()
# add cameras
devices = []
tasks = []
for camera in cameras: for camera in cameras:
if not config.get(CONF_WHITELIST): if not config.get(CONF_WHITELIST):
camera_id = camera['id'] camera_id = camera['id']
snapshot_path = camera['snapshot_path'] snapshot_path = camera['snapshot_path']
add_devices([SynologyCamera(config, device = SynologyCamera(
camera_id, config,
camera['name'], camera_id,
snapshot_path, camera['name'],
streaming_path, snapshot_path,
camera_path, streaming_path,
auth_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.""" """Get a session id."""
auth_payload = {'api': AUTH_API, auth_payload = {
'method': 'Login', 'api': AUTH_API,
'version': '2', 'method': 'Login',
'account': username, 'version': '2',
'passwd': password, 'account': username,
'session': 'SurveillanceStation', 'passwd': password,
'format': 'sid'} 'session': 'SurveillanceStation',
auth_req = requests.get(login_url, 'format': 'sid'
params=auth_payload, }
verify=valid_cert, try:
timeout=TIMEOUT) with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_resp = auth_req.json() 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'] return auth_resp['data']['sid']
@ -148,74 +202,92 @@ class SynologyCamera(Camera):
self._streaming_path = streaming_path self._streaming_path = streaming_path
self._camera_path = camera_path self._camera_path = camera_path
self._auth_path = auth_path self._auth_path = auth_path
self._session_id = None
self._session_id = get_session_id(self._username, @asyncio.coroutine
self._password, def async_read_sid(self):
self._login_url,
self._valid_cert)
def get_sid(self):
"""Get a session id.""" """Get a session id."""
auth_payload = {'api': AUTH_API, self._session_id = yield from get_session_id(
'method': 'Login', self.hass,
'version': '2', self._username,
'account': self._username, self._password,
'passwd': self._password, self._login_url,
'session': 'SurveillanceStation', self._valid_cert
'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']
def camera_image(self): 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.""" """Return a still image response from the camera."""
image_url = SYNO_API_URL.format(self._synology_url, image_url = SYNO_API_URL.format(
WEBAPI_PATH, self._synology_url, WEBAPI_PATH, self._camera_path)
self._camera_path)
image_payload = {'api': CAMERA_API, image_payload = {
'method': 'GetSnapshot', 'api': CAMERA_API,
'version': '1', 'method': 'GetSnapshot',
'cameraId': self._camera_id} 'version': '1',
'cameraId': self._camera_id
}
try: try:
response = requests.get(image_url, with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
params=image_payload, response = yield from self.hass.websession.get(
timeout=TIMEOUT, image_url,
verify=self._valid_cert, params=image_payload,
cookies={'id': self._session_id}) verify_ssl=self._valid_cert,
except requests.exceptions.RequestException as error: cookies={'id': self._session_id}
_LOGGER.error('Error getting camera image: %s', error) )
except asyncio.TimeoutError:
_LOGGER.error("Timeout on %s", image_url)
return None 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.""" """Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(self._synology_url, streaming_url = SYNO_API_URL.format(
WEBAPI_PATH, self._synology_url, WEBAPI_PATH, self._streaming_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
def mjpeg_steam(self, response): streaming_payload = {
"""Generate an HTTP MJPEG Stream from the Synology NAS.""" 'api': STREAMING_API,
stream = self.camera_stream() 'method': 'Stream',
return response( 'version': '1',
stream.iter_content(chunk_size=1024), 'cameraId': self._camera_id,
mimetype=stream.headers['CONTENT_TYPE_HEADER'], 'format': 'mjpeg'
direct_passthrough=True }
) 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 @property
def name(self): def name(self):

View file

@ -17,9 +17,9 @@ import threading
import time import time
from types import MappingProxyType from types import MappingProxyType
from typing import Optional, Any, Callable, List # NOQA from typing import Optional, Any, Callable, List # NOQA
import aiohttp
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
@ -143,6 +143,7 @@ class HomeAssistant(object):
self.config = Config() # type: Config self.config = Config() # type: Config
self.state = CoreState.not_running self.state = CoreState.not_running
self.exit_code = None self.exit_code = None
self.websession = aiohttp.ClientSession(loop=self.loop)
@property @property
def is_running(self) -> bool: def is_running(self) -> bool: