From 3324995e701438afc0ba099e36bb2b50b94895d8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Oct 2016 06:40:10 +0200 Subject: [PATCH] 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 --- homeassistant/components/camera/__init__.py | 6 +- homeassistant/components/camera/generic.py | 6 +- homeassistant/components/camera/mjpeg.py | 13 +- homeassistant/components/camera/synology.py | 292 ++++++++++++-------- homeassistant/core.py | 3 +- 5 files changed, 199 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ce811780856..35a922ee0f1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -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 diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index e6dc8968030..861a7cab758 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -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: diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index e1c39a62572..e92274247de 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -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() diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index dedf91a0031..3b3ba2e41a8 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -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): diff --git a/homeassistant/core.py b/homeassistant/core.py index bd59db59f05..19f1436ffa3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -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: