async HTTP component (#3914)

* Migrate WSGI to asyncio

* Rename wsgi -> http

* Python 3.4 compat

* Move linting to Python 3.4

* lint

* Lint

* Fix Python 3.4 mock_open + binary data

* Surpress logging aiohttp.access

* Spelling

* Sending files is a coroutine

* More callback annotations and naming fixes

* Fix ios
This commit is contained in:
Paulus Schoutsen 2016-10-23 23:48:01 -07:00 committed by GitHub
parent 9aa88819a5
commit 519d9f2fd0
45 changed files with 1422 additions and 1009 deletions

View file

@ -2,11 +2,11 @@ sudo: false
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: "3.4" - python: "3.4.2"
env: TOXENV=py34 env: TOXENV=py34
- python: "3.4" - python: "3.4.2"
env: TOXENV=requirements env: TOXENV=requirements
- python: "3.5" - python: "3.4.2"
env: TOXENV=lint env: TOXENV=lint
- python: "3.5" - python: "3.5"
env: TOXENV=typing env: TOXENV=typing

View file

@ -359,6 +359,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
# suppress overly verbose logs from libraries that aren't helpful # suppress overly verbose logs from libraries that aren't helpful
logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
try: try:
from colorlog import ColoredFormatter from colorlog import ColoredFormatter

View file

@ -4,6 +4,7 @@ Support for Alexa skill service end point.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/ https://home-assistant.io/components/alexa/
""" """
import asyncio
import copy import copy
import enum import enum
import logging import logging
@ -12,6 +13,7 @@ from datetime import datetime
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = '/api/alexa'
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/<briefing_id>' FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_CARD = 'card' CONF_CARD = 'card'
@ -102,8 +104,8 @@ def setup(hass, config):
intents = config[DOMAIN].get(CONF_INTENTS, {}) intents = config[DOMAIN].get(CONF_INTENTS, {})
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {}) flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
hass.wsgi.register_view(AlexaIntentsView(hass, intents)) hass.http.register_view(AlexaIntentsView(hass, intents))
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings)) hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
return True return True
@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView):
self.intents = intents self.intents = intents
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle Alexa.""" """Handle Alexa."""
data = request.json data = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data) _LOGGER.debug('Received Alexa request: %s', data)
@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView):
action = config.get(CONF_ACTION) action = config.get(CONF_ACTION)
if action is not None: if action is not None:
action.run(response.variables) yield from action.async_run(response.variables)
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
if speech is not None: if speech is not None:
@ -218,8 +221,8 @@ class AlexaResponse(object):
self.card = card self.card = card
return return
card["title"] = title.render(self.variables) card["title"] = title.async_render(self.variables)
card["content"] = content.render(self.variables) card["content"] = content.async_render(self.variables)
self.card = card self.card = card
def add_speech(self, speech_type, text): def add_speech(self, speech_type, text):
@ -229,7 +232,7 @@ class AlexaResponse(object):
key = 'ssml' if speech_type == SpeechType.ssml else 'text' key = 'ssml' if speech_type == SpeechType.ssml else 'text'
if isinstance(text, template.Template): if isinstance(text, template.Template):
text = text.render(self.variables) text = text.async_render(self.variables)
self.speech = { self.speech = {
'type': speech_type.value, 'type': speech_type.value,
@ -244,7 +247,7 @@ class AlexaResponse(object):
self.reprompt = { self.reprompt = {
'type': speech_type.value, 'type': speech_type.value,
key: text.render(self.variables) key: text.async_render(self.variables)
} }
def as_dict(self): def as_dict(self):
@ -284,6 +287,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
template.attach(hass, self.flash_briefings) template.attach(hass, self.flash_briefings)
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@callback
def get(self, request, briefing_id): def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug('Received Alexa flash briefing request for: %s', _LOGGER.debug('Received Alexa flash briefing request for: %s',
@ -292,7 +296,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if self.flash_briefings.get(briefing_id) is None: if self.flash_briefings.get(briefing_id) is None:
err = 'No configured Alexa flash briefing was found for: %s' err = 'No configured Alexa flash briefing was found for: %s'
_LOGGER.error(err, briefing_id) _LOGGER.error(err, briefing_id)
return self.Response(status=404) return b'', 404
briefing = [] briefing = []
@ -300,13 +304,13 @@ class AlexaFlashBriefingView(HomeAssistantView):
output = {} output = {}
if item.get(CONF_TITLE) is not None: if item.get(CONF_TITLE) is not None:
if isinstance(item.get(CONF_TITLE), template.Template): if isinstance(item.get(CONF_TITLE), template.Template):
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render() output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
else: else:
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
if item.get(CONF_TEXT) is not None: if item.get(CONF_TEXT) is not None:
if isinstance(item.get(CONF_TEXT), template.Template): if isinstance(item.get(CONF_TEXT), template.Template):
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render() output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
else: else:
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
@ -315,7 +319,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if item.get(CONF_AUDIO) is not None: if item.get(CONF_AUDIO) is not None:
if isinstance(item.get(CONF_AUDIO), template.Template): if isinstance(item.get(CONF_AUDIO), template.Template):
output[ATTR_STREAM_URL] = item[CONF_AUDIO].render() output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
else: else:
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
@ -323,7 +327,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if isinstance(item.get(CONF_DISPLAY_URL), if isinstance(item.get(CONF_DISPLAY_URL),
template.Template): template.Template):
output[ATTR_REDIRECTION_URL] = \ output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].render() item[CONF_DISPLAY_URL].async_render()
else: else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)

View file

@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
import asyncio import asyncio
import json import json
import logging import logging
import queue
from aiohttp import web
import async_timeout
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.remote as rem import homeassistant.remote as rem
@ -21,7 +23,7 @@ from homeassistant.const import (
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__) __version__)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
hass.wsgi.register_view(APIStatusView) hass.http.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream) hass.http.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView) hass.http.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView) hass.http.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView) hass.http.register_view(APIStatesView)
hass.wsgi.register_view(APIEntityStateView) hass.http.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView) hass.http.register_view(APIEventListenersView)
hass.wsgi.register_view(APIEventView) hass.http.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView) hass.http.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView) hass.http.register_view(APIDomainServicesView)
hass.wsgi.register_view(APIEventForwardingView) hass.http.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView) hass.http.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView) hass.http.register_view(APIErrorLogView)
hass.wsgi.register_view(APITemplateView) hass.http.register_view(APITemplateView)
return True return True
@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
url = URL_API url = URL_API
name = "api:status" name = "api:status"
@ha.callback
def get(self, request): def get(self, request):
"""Retrieve if API is running.""" """Retrieve if API is running."""
return self.json_message('API running.') return self.json_message('API running.')
@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = "api:stream"
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
stop_obj = object() stop_obj = object()
to_write = queue.Queue() to_write = asyncio.Queue(loop=self.hass.loop)
restrict = request.args.get('restrict') restrict = request.GET.get('restrict')
if restrict: if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView):
else: else:
data = json.dumps(event, cls=rem.JSONEncoder) data = json.dumps(event, cls=rem.JSONEncoder)
to_write.put(data) yield from to_write.put(data)
def stream(): response = web.StreamResponse()
"""Stream events to response.""" response.content_type = 'text/event-stream'
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events) yield from response.prepare(request)
try: unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
# Fire off one message so browsers fire open event right away try:
to_write.put(STREAM_PING_PAYLOAD) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
while True: # Fire off one message so browsers fire open event right away
try: yield from to_write.put(STREAM_PING_PAYLOAD)
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
if payload is stop_obj: while True:
break try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=self.hass.loop):
payload = yield from to_write.get()
msg = "data: {}\n\n".format(payload) if payload is stop_obj:
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield msg.encode("UTF-8")
except queue.Empty:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit:
break break
finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream()
return self.Response(stream(), mimetype='text/event-stream') msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
response.write(msg.encode("UTF-8"))
yield from response.drain()
except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD)
finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream()
class APIConfigView(HomeAssistantView): class APIConfigView(HomeAssistantView):
@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
url = URL_API_CONFIG url = URL_API_CONFIG
name = "api:config" name = "api:config"
@ha.callback
def get(self, request): def get(self, request):
"""Get current configuration.""" """Get current configuration."""
return self.json(self.hass.config.as_dict()) return self.json(self.hass.config.as_dict())
@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = "api:discovery" name = "api:discovery"
@ha.callback
def get(self, request): def get(self, request):
"""Get discovery info.""" """Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None needs_auth = self.hass.config.api.api_password is not None
@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
url = URL_API_STATES url = URL_API_STATES
name = "api:states" name = "api:states"
@ha.callback
def get(self, request): def get(self, request):
"""Get current states.""" """Get current states."""
return self.json(self.hass.states.all()) return self.json(self.hass.states.async_all())
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests.""" """View to handle EntityState requests."""
url = "/api/states/<entity(exist=False):entity_id>" url = "/api/states/{entity_id}"
name = "api:entity-state" name = "api:entity-state"
@ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity.""" """Retrieve state of entity."""
state = self.hass.states.get(entity_id) state = self.hass.states.get(entity_id)
@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
else: else:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine
def post(self, request, entity_id): def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
try: try:
new_state = request.json['state'] data = yield from request.json()
except KeyError: except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
new_state = data.get('state')
if not new_state:
return self.json_message('No state specified', HTTP_BAD_REQUEST) return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.json.get('attributes') attributes = data.get('attributes')
force_update = request.json.get('force_update', False) force_update = data.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None is_new_state = self.hass.states.get(entity_id) is None
# Write state # Write state
self.hass.states.set(entity_id, new_state, attributes, force_update) self.hass.states.async_set(entity_id, new_state, attributes,
force_update)
# Read the state back for our response # Read the state back for our response
resp = self.json(self.hass.states.get(entity_id)) status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(self.hass.states.get(entity_id), status_code)
if is_new_state:
resp.status_code = HTTP_CREATED
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
return resp return resp
@ha.callback
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if self.hass.states.remove(entity_id): if self.hass.states.async_remove(entity_id):
return self.json_message('Entity removed') return self.json_message('Entity removed')
else: else:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
url = URL_API_EVENTS url = URL_API_EVENTS
name = "api:event-listeners" name = "api:event-listeners"
@ha.callback
def get(self, request): def get(self, request):
"""Get event listeners.""" """Get event listeners."""
return self.json(events_json(self.hass)) return self.json(async_events_json(self.hass))
class APIEventView(HomeAssistantView): class APIEventView(HomeAssistantView):
"""View to handle Event requests.""" """View to handle Event requests."""
url = '/api/events/<event_type>' url = '/api/events/{event_type}'
name = "api:event" name = "api:event"
@asyncio.coroutine
def post(self, request, event_type): def post(self, request, event_type):
"""Fire events.""" """Fire events."""
event_data = request.json body = yield from request.text()
event_data = json.loads(body) if body else None
if event_data is not None and not isinstance(event_data, dict): if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object', return self.json_message('Event data should be a JSON object',
@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = state event_data[key] = state
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote) self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = "api:services"
@ha.callback
def get(self, request): def get(self, request):
"""Get registered services.""" """Get registered services."""
return self.json(services_json(self.hass)) return self.json(async_services_json(self.hass))
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests.""" """View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>" url = "/api/services/{domain}/{service}"
name = "api:domain-services" name = "api:domain-services"
@asyncio.coroutine
def post(self, request, domain, service): def post(self, request, domain, service):
"""Call a service. """Call a service.
Returns a list of changed states. Returns a list of changed states.
""" """
with TrackStates(self.hass) as changed_states: body = yield from request.text()
self.hass.services.call(domain, service, request.json, True) data = json.loads(body) if body else None
with AsyncTrackStates(self.hass) as changed_states:
yield from self.hass.services.async_call(domain, service, data,
True)
return self.json(changed_states) return self.json(changed_states)
@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
name = "api:event-forward" name = "api:event-forward"
event_forwarder = None event_forwarder = None
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Setup an event forwarder.""" """Setup an event forwarder."""
data = request.json try:
if data is None: data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST) return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
host = data['host'] host = data['host']
api_password = data['api_password'] api_password = data['api_password']
@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
api = rem.API(host, api_password, port) api = rem.API(host, api_password, port)
if not api.validate_api(): valid = yield from self.hass.loop.run_in_executor(
None, api.validate_api)
if not valid:
return self.json_message("Unable to validate API.", return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None: if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass) self.event_forwarder = rem.EventForwarder(self.hass)
self.event_forwarder.connect(api) self.event_forwarder.async_connect(api)
return self.json_message("Event forwarding setup.") return self.json_message("Event forwarding setup.")
@asyncio.coroutine
def delete(self, request): def delete(self, request):
"""Remove event forwarer.""" """Remove event forwarder."""
data = request.json try:
if data is None: data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST) return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
if self.event_forwarder is not None: if self.event_forwarder is not None:
api = rem.API(host, None, port) api = rem.API(host, None, port)
self.event_forwarder.disconnect(api) self.event_forwarder.async_disconnect(api)
return self.json_message("Event forwarding cancelled.") return self.json_message("Event forwarding cancelled.")
@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = "api:components" name = "api:components"
@ha.callback
def get(self, request): def get(self, request):
"""Get current loaded components.""" """Get current loaded components."""
return self.json(self.hass.config.components) return self.json(self.hass.config.components)
@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = "api:error-log" name = "api:error-log"
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Serve error log.""" """Serve error log."""
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME)) resp = yield from self.file(
request, self.hass.config.path(ERROR_LOG_FILENAME))
return resp
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = "api:template"
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Render a template.""" """Render a template."""
try: try:
tpl = template.Template(request.json['template'], self.hass) data = yield from request.json()
return tpl.render(request.json.get('variables')) tpl = template.Template(data['template'], self.hass)
except TemplateError as ex: return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex), return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
def services_json(hass): def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
return [{"domain": key, "services": value} return [{"domain": key, "services": value}
for key, value in hass.services.services.items()] for key, value in hass.services.async_services().items()]
def events_json(hass): def async_events_json(hass):
"""Generate event data to JSONify.""" """Generate event data to JSONify."""
return [{"event": key, "listener_count": value} return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()] for key, value in hass.bus.async_listeners().items()]

View file

@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
from haffmpeg import SensorNoise, SensorMotion from haffmpeg import SensorNoise, SensorMotion
# check source # check source
if not run_test(config.get(CONF_INPUT)): if not run_test(hass, config.get(CONF_INPUT)):
return return
# generate sensor object # generate sensor object

View file

@ -5,8 +5,10 @@ Component to interface with cameras.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import asyncio
import logging import logging
import time
from aiohttp import web
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -31,8 +33,8 @@ def setup(hass, config):
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(CameraImageView(hass, component.entities)) hass.http.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities)) hass.http.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config) component.setup(config)
@ -80,33 +82,59 @@ class Camera(Entity):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
def mjpeg_stream(self, response): @asyncio.coroutine
"""Generate an HTTP MJPEG stream from camera images.""" def async_camera_image(self):
def stream(): """Return bytes of camera image.
"""Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
if img_bytes is not None and img_bytes != last_image: This method must be run in the event loop.
yield bytes( """
'--jpegboundary\r\n' image = yield from self.hass.loop.run_in_executor(
'Content-Type: image/jpeg\r\n' None, self.camera_image)
'Content-Length: {}\r\n\r\n'.format( return image
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
last_image = img_bytes @asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images.
time.sleep(0.5) This method must be run in the event loop.
except GeneratorExit: """
pass response = web.StreamResponse()
return response( response.content_type = ('multipart/x-mixed-replace; '
stream(), 'boundary=--jpegboundary')
content_type=('multipart/x-mixed-replace; ' response.enable_chunked_encoding()
'boundary=--jpegboundary') yield from response.prepare(request)
)
def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
last_image = None
try:
while True:
img_bytes = yield from self.async_camera_image()
if not img_bytes:
break
if img_bytes is not None and img_bytes != last_image:
write(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
write(img_bytes)
last_image = img_bytes
yield from response.drain()
yield from asyncio.sleep(.5)
finally:
self.hass.loop.create_task(response.write_eof())
@property @property
def state(self): def state(self):
@ -144,22 +172,25 @@ class CameraView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.entities = entities self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id): def get(self, request, entity_id):
"""Start a get request.""" """Start a get request."""
camera = self.entities.get(entity_id) camera = self.entities.get(entity_id)
if camera is None: if camera is None:
return self.Response(status=404) return web.Response(status=404)
authenticated = (request.authenticated or authenticated = (request.authenticated or
request.args.get('token') == camera.access_token) request.GET.get('token') == camera.access_token)
if not authenticated: if not authenticated:
return self.Response(status=401) return web.Response(status=401)
return self.handle(camera) response = yield from self.handle(request, camera)
return response
def handle(self, camera): @asyncio.coroutine
def handle(self, request, camera):
"""Hanlde the camera request.""" """Hanlde the camera request."""
raise NotImplementedError() raise NotImplementedError()
@ -167,25 +198,27 @@ class CameraView(HomeAssistantView):
class CameraImageView(CameraView): class CameraImageView(CameraView):
"""Camera view to serve an image.""" """Camera view to serve an image."""
url = "/api/camera_proxy/<entity(domain=camera):entity_id>" url = "/api/camera_proxy/{entity_id}"
name = "api:camera:image" name = "api:camera:image"
def handle(self, camera): @asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
response = camera.camera_image() image = yield from camera.async_camera_image()
if response is None: if image is None:
return self.Response(status=500) return web.Response(status=500)
return self.Response(response) return web.Response(body=image)
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):
"""Camera View to serve an MJPEG stream.""" """Camera View to serve an MJPEG stream."""
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>" url = "/api/camera_proxy_stream/{entity_id}"
name = "api:camera:stream" name = "api:camera:stream"
def handle(self, camera): @asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
return camera.mjpeg_stream(self.Response) yield from camera.handle_async_mjpeg_stream(request)

View file

@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
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.ffmpeg/ https://home-assistant.io/components/camera.ffmpeg/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from aiohttp import web
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.ffmpeg import ( from homeassistant.components.ffmpeg import (
run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.util.async import run_coroutine_threadsafe
DEPENDENCIES = ['ffmpeg'] DEPENDENCIES = ['ffmpeg']
@ -27,17 +30,18 @@ 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 FFmpeg Camera.""" """Setup a FFmpeg Camera."""
if not run_test(config.get(CONF_INPUT)): if not async_run_test(hass, config.get(CONF_INPUT)):
return return
add_devices([FFmpegCamera(config)]) hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
class FFmpegCamera(Camera): class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera.""" """An implementation of an FFmpeg camera."""
def __init__(self, config): def __init__(self, hass, config):
"""Initialize a FFmpeg camera.""" """Initialize a FFmpeg camera."""
super().__init__() super().__init__()
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
@ -45,24 +49,45 @@ class FFmpegCamera(Camera):
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
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."""
from haffmpeg import ImageSingle, IMAGE_JPEG from haffmpeg import ImageSingleAsync, IMAGE_JPEG
ffmpeg = ImageSingle(get_binary()) ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, image = yield from ffmpeg.get_image(
extra_cmd=self._extra_arguments) self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments)
return image
def mjpeg_stream(self, response): @asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpegAsync
stream = CameraMjpeg(get_binary()) stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
stream.open_camera(self._input, extra_cmd=self._extra_arguments) yield from stream.open_camera(
return response( self._input, extra_cmd=self._extra_arguments)
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver', response = web.StreamResponse()
direct_passthrough=True response.content_type = 'multipart/x-mixed-replace;boundary=ffserver'
) response.enable_chunked_encoding()
yield from response.prepare(request)
try:
while True:
data = yield from stream.read(102400)
if not data:
break
response.write(data)
finally:
self.hass.loop.create_task(stream.close())
self.hass.loop.create_task(response.write_eof())
@property @property
def name(self): def name(self):

View file

@ -4,10 +4,13 @@ Support for IP 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.generic/ https://home-assistant.io/components/camera.generic/
""" """
import asyncio
import logging import logging
import aiohttp
import async_timeout
import requests import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPDigestAuth
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a generic IP Camera.""" """Setup a generic IP Camera."""
add_devices([GenericCamera(hass, config)]) hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
@ -49,6 +54,7 @@ class GenericCamera(Camera):
"""Initialize a generic camera.""" """Initialize a generic camera."""
super().__init__() super().__init__()
self.hass = hass self.hass = hass
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass self._still_image_url.hass = hass
@ -58,20 +64,27 @@ class GenericCamera(Camera):
password = device_info.get(CONF_PASSWORD) password = device_info.get(CONF_PASSWORD)
if username and password: if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password) self._auth = HTTPDigestAuth(username, password)
else: else:
self._auth = HTTPBasicAuth(username, password) self._auth = aiohttp.BasicAuth(username, password=password)
else: else:
self._auth = None self._auth = None
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 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."""
try: try:
url = self._still_image_url.render() url = self._still_image_url.async_render()
except TemplateError as err: except TemplateError as err:
_LOGGER.error('Error parsing template %s: %s', _LOGGER.error('Error parsing template %s: %s',
self._still_image_url, err) self._still_image_url, err)
@ -80,16 +93,32 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch: if url == self._last_url and self._limit_refetch:
return self._last_image return self._last_image
kwargs = {'timeout': 10, 'auth': self._auth} # aiohttp don't support DigestAuth jet
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
def fetch():
"""Read image from a URL."""
try:
kwargs = {'timeout': 10, 'auth': self._auth}
response = requests.get(url, **kwargs)
return response.content
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return self._last_image
try: self._last_image = yield from self.hass.loop.run_in_executor(
response = requests.get(url, **kwargs) None, fetch)
except requests.exceptions.RequestException as error: # async
_LOGGER.error('Error getting camera image: %s', error) else:
return None try:
with async_timeout.timeout(10, loop=self.hass.loop):
respone = yield from self._session.get(url)
self._last_image = yield from respone.read()
self.hass.loop.create_task(respone.release())
except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image')
return self._last_image
self._last_url = url self._last_url = url
self._last_image = response.content
return self._last_image return self._last_image
@property @property

View file

@ -4,9 +4,14 @@ Support for IP 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.mjpeg/ https://home-assistant.io/components/camera.mjpeg/
""" """
import asyncio
import logging import logging
from contextlib import closing from contextlib import closing
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
import requests import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol import voluptuous as vol
@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera.""" """Setup a MJPEG IP Camera."""
add_devices([MjpegCamera(config)]) hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):
@ -56,7 +62,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera): class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL.""" """An implementation of an IP camera that is reachable over a URL."""
def __init__(self, device_info): def __init__(self, hass, device_info):
"""Initialize a MJPEG camera.""" """Initialize a MJPEG camera."""
super().__init__() super().__init__()
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
@ -65,32 +71,57 @@ 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]
def camera_stream(self): auth = None
"""Return a MJPEG stream image response directly from the camera.""" if self._authentication == HTTP_BASIC_AUTHENTICATION:
auth = aiohttp.BasicAuth(self._username, password=self._password)
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth)
def camera_image(self):
"""Return a still image response from the camera."""
if self._username and self._password: if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password) auth = HTTPDigestAuth(self._username, self._password)
else: else:
auth = HTTPBasicAuth(self._username, self._password) auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url, req = requests.get(
auth=auth, self._mjpeg_url, auth=auth, stream=True, timeout=10)
stream=True, timeout=10)
else: else:
return requests.get(self._mjpeg_url, stream=True, timeout=10) req = requests.get(self._mjpeg_url, stream=True, timeout=10)
def camera_image(self): with closing(req) as response:
"""Return a still image response from the camera.""" return extract_image_from_mjpeg(response.iter_content(102400))
with closing(self.camera_stream()) as response:
return extract_image_from_mjpeg(response.iter_content(1024))
def mjpeg_stream(self, response): @asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
stream = self.camera_stream() # aiohttp don't support DigestAuth -> Fallback
return response( if self._authentication == HTTP_DIGEST_AUTHENTICATION:
stream.iter_content(chunk_size=1024), yield from super().handle_async_mjpeg_stream(request)
mimetype=stream.headers[CONTENT_TYPE_HEADER], return
direct_passthrough=True
) # connect to stream
try:
with async_timeout.timeout(10, loop=self.hass.loop):
stream = yield from self._session.get(self._mjpeg_url)
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

@ -4,6 +4,8 @@ Support for the Locative platform.
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/device_tracker.locative/ https://home-assistant.io/components/device_tracker.locative/
""" """
import asyncio
from functools import partial
import logging import logging
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
@ -19,7 +21,7 @@ DEPENDENCIES = ['http']
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup an endpoint for the Locative application.""" """Setup an endpoint for the Locative application."""
hass.wsgi.register_view(LocativeView(hass, see)) hass.http.register_view(LocativeView(hass, see))
return True return True
@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.see = see self.see = see
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Locative message received as GET.""" """Locative message received as GET."""
return self.post(request) res = yield from self._handle(request.GET)
return res
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Locative message received.""" """Locative message received."""
# pylint: disable=too-many-return-statements data = yield from request.post()
data = request.values res = yield from self._handle(data)
return res
@asyncio.coroutine
def _handle(self, data):
"""Handle locative request."""
# pylint: disable=too-many-return-statements
if 'latitude' not in data or 'longitude' not in data: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView):
direction = data['trigger'] direction = data['trigger']
if direction == 'enter': if direction == 'enter':
self.see(dev_id=device, location_name=location_name) yield from self.hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name))
return 'Setting location to {}'.format(location_name) return 'Setting location to {}'.format(location_name)
elif direction == 'exit': elif direction == 'exit':
@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView):
'{}.{}'.format(DOMAIN, device)) '{}.{}'.format(DOMAIN, device))
if current_state is None or current_state.state == location_name: if current_state is None or current_state.state == location_name:
self.see(dev_id=device, location_name=STATE_NOT_HOME) yield from self.hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=STATE_NOT_HOME))
return 'Setting location to not home' return 'Setting location to not home'
else: else:
# Ignore the message if it is telling us to exit a zone that we # Ignore the message if it is telling us to exit a zone that we

View file

@ -4,20 +4,21 @@ Support for local control of entities by emulating the Phillips Hue bridge.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio
import threading import threading
import socket import socket
import logging import logging
import json
import os import os
import select import select
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant import util, core from homeassistant import util, core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON, HTTP_BAD_REQUEST STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@ -25,8 +26,6 @@ from homeassistant.components.light import (
from homeassistant.components.http import ( from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI HomeAssistantView, HomeAssistantWSGI
) )
# pylint: disable=unused-import
from homeassistant.components.http import REQUIREMENTS # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue' DOMAIN = 'emulated_hue'
@ -87,19 +86,21 @@ def setup(hass, yaml_config):
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port) config.host_ip_addr, config.listen_port)
def start_emulated_hue_bridge(event): @core.callback
"""Start the emulated hue bridge."""
server.start()
upnp_listener.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
def stop_emulated_hue_bridge(event): def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge.""" """Stop the emulated hue bridge."""
upnp_listener.stop() upnp_listener.stop()
server.stop() hass.loop.create_task(server.stop())
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) @core.callback
def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
hass.loop.create_task(server.start())
upnp_listener.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
stop_emulated_hue_bridge)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
return True return True
@ -158,6 +159,7 @@ class DescriptionXmlView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.config = config self.config = config
@core.callback
def get(self, request): def get(self, request):
"""Handle a GET request.""" """Handle a GET request."""
xml_template = """<?xml version="1.0" encoding="UTF-8" ?> xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
@ -185,7 +187,7 @@ class DescriptionXmlView(HomeAssistantView):
resp_text = xml_template.format( resp_text = xml_template.format(
self.config.host_ip_addr, self.config.listen_port) self.config.host_ip_addr, self.config.listen_port)
return self.Response(resp_text, mimetype='text/xml') return web.Response(text=resp_text, content_type='text/xml')
class HueUsernameView(HomeAssistantView): class HueUsernameView(HomeAssistantView):
@ -200,9 +202,13 @@ class HueUsernameView(HomeAssistantView):
"""Initialize the instance of the view.""" """Initialize the instance of the view."""
super().__init__(hass) super().__init__(hass)
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle a POST request.""" """Handle a POST request."""
data = request.json try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
if 'devicetype' not in data: if 'devicetype' not in data:
return self.json_message('devicetype not specified', return self.json_message('devicetype not specified',
@ -214,10 +220,10 @@ class HueUsernameView(HomeAssistantView):
class HueLightsView(HomeAssistantView): class HueLightsView(HomeAssistantView):
"""Handle requests for getting and setting info about entities.""" """Handle requests for getting and setting info about entities."""
url = '/api/<username>/lights' url = '/api/{username}/lights'
name = 'api:username:lights' name = 'api:username:lights'
extra_urls = ['/api/<username>/lights/<entity_id>', extra_urls = ['/api/{username}/lights/{entity_id}',
'/api/<username>/lights/<entity_id>/state'] '/api/{username}/lights/{entity_id}/state']
requires_auth = False requires_auth = False
def __init__(self, hass, config): def __init__(self, hass, config):
@ -226,58 +232,51 @@ class HueLightsView(HomeAssistantView):
self.config = config self.config = config
self.cached_states = {} self.cached_states = {}
@core.callback
def get(self, request, username, entity_id=None): def get(self, request, username, entity_id=None):
"""Handle a GET request.""" """Handle a GET request."""
if entity_id is None: if entity_id is None:
return self.get_lights_list() return self.async_get_lights_list()
if not request.base_url.endswith('state'): if not request.path.endswith('state'):
return self.get_light_state(entity_id) return self.async_get_light_state(entity_id)
return self.Response("Method not allowed", status=405) return web.Response(text="Method not allowed", status=405)
@asyncio.coroutine
def put(self, request, username, entity_id=None): def put(self, request, username, entity_id=None):
"""Handle a PUT request.""" """Handle a PUT request."""
if not request.base_url.endswith('state'): if not request.path.endswith('state'):
return self.Response("Method not allowed", status=405) return web.Response(text="Method not allowed", status=405)
content_type = request.environ.get('CONTENT_TYPE', '') if entity_id and self.hass.states.get(entity_id) is None:
if content_type == 'application/x-www-form-urlencoded': return self.json_message('Entity not found', HTTP_NOT_FOUND)
# Alexa sends JSON data with a form data content type, for
# whatever reason, and Werkzeug parses form data automatically,
# so we need to do some gymnastics to get the data we need
json_data = None
for key in request.form: try:
try: json_data = yield from request.json()
json_data = json.loads(key) except ValueError:
break return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
except ValueError:
# Try the next key?
pass
if json_data is None: result = yield from self.async_put_light_state(json_data, entity_id)
return self.Response("Bad request", status=400) return result
else:
json_data = request.json
return self.put_light_state(json_data, entity_id) @core.callback
def async_get_lights_list(self):
def get_lights_list(self):
"""Process a request to get the list of available lights.""" """Process a request to get the list of available lights."""
json_response = {} json_response = {}
for entity in self.hass.states.all(): for entity in self.hass.states.async_all():
if self.is_entity_exposed(entity): if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity) json_response[entity.entity_id] = entity_to_json(entity)
return self.json(json_response) return self.json(json_response)
def get_light_state(self, entity_id): @core.callback
def async_get_light_state(self, entity_id):
"""Process a request to get the state of an individual light.""" """Process a request to get the state of an individual light."""
entity = self.hass.states.get(entity_id) entity = self.hass.states.get(entity_id)
if entity is None or not self.is_entity_exposed(entity): if entity is None or not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404) return web.Response(text="Entity not found", status=404)
cached_state = self.cached_states.get(entity_id, None) cached_state = self.cached_states.get(entity_id, None)
@ -292,23 +291,24 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response) return self.json(json_response)
def put_light_state(self, request_json, entity_id): @asyncio.coroutine
def async_put_light_state(self, request_json, entity_id):
"""Process a request to set the state of an individual light.""" """Process a request to set the state of an individual light."""
config = self.config config = self.config
# Retrieve the entity from the state machine # Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id) entity = self.hass.states.get(entity_id)
if entity is None: if entity is None:
return self.Response("Entity not found", status=404) return web.Response(text="Entity not found", status=404)
if not self.is_entity_exposed(entity): if not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404) return web.Response(text="Entity not found", status=404)
# Parse the request into requested "on" status and brightness # Parse the request into requested "on" status and brightness
parsed = parse_hue_api_put_light_body(request_json, entity) parsed = parse_hue_api_put_light_body(request_json, entity)
if parsed is None: if parsed is None:
return self.Response("Bad request", status=400) return web.Response(text="Bad request", status=400)
result, brightness = parsed result, brightness = parsed
@ -333,7 +333,8 @@ class HueLightsView(HomeAssistantView):
self.cached_states[entity_id] = (result, brightness) self.cached_states[entity_id] = (result, brightness)
# Perform the requested action # Perform the requested action
self.hass.services.call(core.DOMAIN, service, data, blocking=True) yield from self.hass.services.async_call(core.DOMAIN, service, data,
blocking=True)
json_response = \ json_response = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
@ -345,7 +346,10 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response) return self.json(json_response)
def is_entity_exposed(self, entity): def is_entity_exposed(self, entity):
"""Determine if an entity should be exposed on the emulated bridge.""" """Determine if an entity should be exposed on the emulated bridge.
Async friendly.
"""
config = self.config config = self.config
if entity.attributes.get('view') is not None: if entity.attributes.get('view') is not None:

View file

@ -4,14 +4,16 @@ Component that will help set the ffmpeg component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ffmpeg/ https://home-assistant.io/components/ffmpeg/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'
REQUIREMENTS = ["ha-ffmpeg==0.13"] REQUIREMENTS = ["ha-ffmpeg==0.14"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,13 +49,26 @@ def setup(hass, config):
def get_binary(): def get_binary():
"""Return ffmpeg binary from config.""" """Return ffmpeg binary from config.
Async friendly.
"""
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN)
def run_test(input_source): def run_test(hass, input_source):
"""Run test on this input. TRUE is deactivate or run correct.""" """Run test on this input. TRUE is deactivate or run correct."""
from haffmpeg import Test return run_coroutine_threadsafe(
async_run_test(hass, input_source), hass.loop).result()
@asyncio.coroutine
def async_run_test(hass, input_source):
"""Run test on this input. TRUE is deactivate or run correct.
This method must be run in the event loop.
"""
from haffmpeg import TestAsync
if FFMPEG_CONFIG.get(CONF_RUN_TEST): if FFMPEG_CONFIG.get(CONF_RUN_TEST):
# if in cache # if in cache
@ -61,8 +76,9 @@ def run_test(input_source):
return FFMPEG_TEST_CACHE[input_source] return FFMPEG_TEST_CACHE[input_source]
# run test # run test
test = Test(get_binary()) ffmpeg_test = TestAsync(get_binary(), loop=hass.loop)
if not test.run_test(input_source): success = yield from ffmpeg_test.run_test(input_source)
if not success:
_LOGGER.error("FFmpeg '%s' test fails!", input_source) _LOGGER.error("FFmpeg '%s' test fails!", input_source)
FFMPEG_TEST_CACHE[input_source] = False FFMPEG_TEST_CACHE[input_source] = False
return False return False

View file

@ -4,14 +4,14 @@ Allows utilizing the Foursquare (Swarm) API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/foursquare/ https://home-assistant.io/components/foursquare/
""" """
import asyncio
import logging import logging
import os import os
import json
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -75,7 +75,7 @@ def setup(hass, config):
descriptions[DOMAIN][SERVICE_CHECKIN], descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA) schema=CHECKIN_SERVICE_SCHEMA)
hass.wsgi.register_view(FoursquarePushReceiver( hass.http.register_view(FoursquarePushReceiver(
hass, config[CONF_PUSH_SECRET])) hass, config[CONF_PUSH_SECRET]))
return True return True
@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.push_secret = push_secret self.push_secret = push_secret
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Accept the POST from Foursquare.""" """Accept the POST from Foursquare."""
raw_data = request.form try:
_LOGGER.debug("Received Foursquare push: %s", raw_data) data = yield from request.json()
if self.push_secret != raw_data["secret"]: except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
secret = data.pop('secret', None)
_LOGGER.debug("Received Foursquare push: %s", data)
if self.push_secret != secret:
_LOGGER.error("Received Foursquare push with invalid" _LOGGER.error("Received Foursquare push with invalid"
"push secret! Data: %s", raw_data) "push secret: %s", secret)
return return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
parsed_payload = {
key: json.loads(val) for key, val in raw_data.items() self.hass.bus.async_fire(EVENT_PUSH, data)
if key != "secret"
}
self.hass.bus.fire(EVENT_PUSH, parsed_payload)

View file

@ -1,8 +1,13 @@
"""Handle the frontend for Home Assistant.""" """Handle the frontend for Home Assistant."""
import asyncio
import hashlib import hashlib
import json
import logging import logging
import os import os
from aiohttp import web
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -39,7 +44,7 @@ def register_built_in_panel(hass, component_name, sidebar_title=None,
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name) path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development: if hass.http.development:
url = ('/static/home-assistant-polymer/panels/' url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name)) '{0}/ha-panel-{0}.html'.format(component_name))
else: else:
@ -98,7 +103,7 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
url = URL_PANEL_COMPONENT.format(component_name) url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS: if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path) hass.http.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url) _REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
@ -114,20 +119,23 @@ def add_manifest_json_key(key, val):
def setup(hass, config): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
hass.wsgi.register_view(BootstrapView) hass.http.register_view(BootstrapView)
hass.wsgi.register_view(ManifestJSONView) hass.http.register_view(ManifestJSONView)
if hass.wsgi.development: if hass.http.development:
sw_path = "home-assistant-polymer/build/service_worker.js" sw_path = "home-assistant-polymer/build/service_worker.js"
else: else:
sw_path = "service_worker.js" sw_path = "service_worker.js"
hass.wsgi.register_static_path("/service_worker.js", hass.http.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), 0) os.path.join(STATIC_PATH, sw_path), 0)
hass.wsgi.register_static_path("/robots.txt", hass.http.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt")) os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH) hass.http.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
local = hass.config.path('www')
if os.path.isdir(local):
hass.http.register_static_path("/local", local)
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
@ -140,7 +148,7 @@ def setup(hass, config):
Done when Home Assistant is started so that all panels are known. Done when Home Assistant is started so that all panels are known.
""" """
hass.wsgi.register_view(IndexView( hass.http.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS])) hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
@ -161,13 +169,14 @@ class BootstrapView(HomeAssistantView):
url = "/api/bootstrap" url = "/api/bootstrap"
name = "api:bootstrap" name = "api:bootstrap"
@callback
def get(self, request): def get(self, request):
"""Return all data needed to bootstrap Home Assistant.""" """Return all data needed to bootstrap Home Assistant."""
return self.json({ return self.json({
'config': self.hass.config.as_dict(), 'config': self.hass.config.as_dict(),
'states': self.hass.states.all(), 'states': self.hass.states.async_all(),
'events': api.events_json(self.hass), 'events': api.async_events_json(self.hass),
'services': api.services_json(self.hass), 'services': api.async_services_json(self.hass),
'panels': PANELS, 'panels': PANELS,
}) })
@ -193,9 +202,10 @@ class IndexView(HomeAssistantView):
) )
) )
@asyncio.coroutine
def get(self, request, entity_id=None): def get(self, request, entity_id=None):
"""Serve the index view.""" """Serve the index view."""
if self.hass.wsgi.development: if self.hass.http.development:
core_url = '/static/home-assistant-polymer/build/core.js' core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html' ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else: else:
@ -215,22 +225,24 @@ class IndexView(HomeAssistantView):
if self.hass.config.api.api_password: if self.hass.config.api.api_password:
# require password if set # require password if set
no_auth = 'false' no_auth = 'false'
if self.hass.wsgi.is_trusted_ip( if self.hass.http.is_trusted_ip(
self.hass.wsgi.get_real_ip(request)): self.hass.http.get_real_ip(request)):
# bypass for trusted networks # bypass for trusted networks
no_auth = 'true' no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html') template = yield from self.hass.loop.run_in_executor(
None, self.templates.get_template, 'index.html')
# pylint is wrong # pylint is wrong
# pylint: disable=no-member # pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=PANELS) panel_url=panel_url, panels=PANELS)
return self.Response(resp, mimetype='text/html') return web.Response(text=resp, content_type='text/html')
class ManifestJSONView(HomeAssistantView): class ManifestJSONView(HomeAssistantView):
@ -240,8 +252,8 @@ class ManifestJSONView(HomeAssistantView):
url = "/manifest.json" url = "/manifest.json"
name = "manifestjson" name = "manifestjson"
def get(self, request): @asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json.""" """Return the manifest.json."""
import json
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return self.Response(msg, mimetype="application/manifest+json") return web.Response(body=msg, content_type="application/manifest+json")

View file

@ -4,11 +4,13 @@ Provide pre-made queries on top of the recorder component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history/ https://home-assistant.io/components/history/
""" """
import asyncio
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
import voluptuous as vol import voluptuous as vol
from homeassistant.const import HTTP_BAD_REQUEST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script from homeassistant.components import recorder, script
@ -182,8 +184,8 @@ def setup(hass, config):
filters.included_entities = include[CONF_ENTITIES] filters.included_entities = include[CONF_ENTITIES]
filters.included_domains = include[CONF_DOMAINS] filters.included_domains = include[CONF_DOMAINS]
hass.wsgi.register_view(Last5StatesView(hass)) hass.http.register_view(Last5StatesView(hass))
hass.wsgi.register_view(HistoryPeriodView(hass, filters)) hass.http.register_view(HistoryPeriodView(hass, filters))
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True return True
@ -192,16 +194,19 @@ def setup(hass, config):
class Last5StatesView(HomeAssistantView): class Last5StatesView(HomeAssistantView):
"""Handle last 5 state view requests.""" """Handle last 5 state view requests."""
url = '/api/history/entity/<entity:entity_id>/recent_states' url = '/api/history/entity/{entity_id}/recent_states'
name = 'api:history:entity-recent-states' name = 'api:history:entity-recent-states'
def __init__(self, hass): def __init__(self, hass):
"""Initilalize the history last 5 states view.""" """Initilalize the history last 5 states view."""
super().__init__(hass) super().__init__(hass)
@asyncio.coroutine
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve last 5 states of entity.""" """Retrieve last 5 states of entity."""
return self.json(last_5_states(entity_id)) result = yield from self.hass.loop.run_in_executor(
None, last_5_states, entity_id)
return self.json(result)
class HistoryPeriodView(HomeAssistantView): class HistoryPeriodView(HomeAssistantView):
@ -209,15 +214,22 @@ class HistoryPeriodView(HomeAssistantView):
url = '/api/history/period' url = '/api/history/period'
name = 'api:history:view-period' name = 'api:history:view-period'
extra_urls = ['/api/history/period/<datetime:datetime>'] extra_urls = ['/api/history/period/{datetime}']
def __init__(self, hass, filters): def __init__(self, hass, filters):
"""Initilalize the history period view.""" """Initilalize the history period view."""
super().__init__(hass) super().__init__(hass)
self.filters = filters self.filters = filters
@asyncio.coroutine
def get(self, request, datetime=None): def get(self, request, datetime=None):
"""Return history over a period of time.""" """Return history over a period of time."""
if datetime:
datetime = dt_util.parse_datetime(datetime)
if datetime is None:
return self.json_message('Invalid datetime', HTTP_BAD_REQUEST)
one_day = timedelta(days=1) one_day = timedelta(days=1)
if datetime: if datetime:
@ -226,10 +238,13 @@ class HistoryPeriodView(HomeAssistantView):
start_time = dt_util.utcnow() - one_day start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day end_time = start_time + one_day
entity_id = request.args.get('filter_entity_id') entity_id = request.GET.get('filter_entity_id')
return self.json(get_significant_states( result = yield from self.hass.loop.run_in_executor(
start_time, end_time, entity_id, self.filters).values()) None, get_significant_states, start_time, end_time, entity_id,
self.filters)
return self.json(result.values())
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods

View file

@ -4,31 +4,36 @@ This module provides WSGI application to serve the Home Assistant API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/ https://home-assistant.io/components/http/
""" """
import asyncio
import hmac import hmac
import json import json
import logging import logging
import mimetypes import mimetypes
import threading import os
from pathlib import Path
import re import re
import ssl import ssl
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
import voluptuous as vol import voluptuous as vol
from aiohttp import web, hdrs
from aiohttp.file_sender import FileSender
from aiohttp.web_exceptions import (
HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified)
from aiohttp.web_urldispatcher import StaticRoute
from homeassistant.core import callback, is_callback
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant import util from homeassistant import util
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE_JSON, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_START)
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import split_entity_id
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
DOMAIN = 'http' DOMAIN = 'http'
REQUIREMENTS = ('cherrypy==8.1.2', 'static3==0.7.0', 'Werkzeug==0.11.11') REQUIREMENTS = ('aiohttp_cors==0.4.0',)
CONF_API_PASSWORD = 'api_password' CONF_API_PASSWORD = 'api_password'
CONF_SERVER_HOST = 'server_host' CONF_SERVER_HOST = 'server_host'
@ -83,6 +88,12 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# TEMP TO GET TESTS TO RUN
def request_class():
"""."""
raise Exception('not implemented')
class HideSensitiveFilter(logging.Filter): class HideSensitiveFilter(logging.Filter):
"""Filter API password calls.""" """Filter API password calls."""
@ -94,17 +105,17 @@ class HideSensitiveFilter(logging.Filter):
def filter(self, record): def filter(self, record):
"""Hide sensitive data in messages.""" """Hide sensitive data in messages."""
if self.hass.wsgi.api_password is None: if self.hass.http.api_password is None:
return True return True
record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******') record.msg = record.msg.replace(self.hass.http.api_password, '*******')
return True return True
def setup(hass, config): def setup(hass, config):
"""Set up the HTTP API and debug interface.""" """Set up the HTTP API and debug interface."""
_LOGGER.addFilter(HideSensitiveFilter(hass)) logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass))
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
@ -131,19 +142,20 @@ def setup(hass, config):
trusted_networks=trusted_networks trusted_networks=trusted_networks
) )
def start_wsgi_server(event): @callback
"""Start the WSGI server.""" def stop_server(event):
server.start() """Callback to stop the server."""
hass.loop.create_task(server.stop())
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server) @callback
def start_server(event):
"""Callback to start the server."""
hass.loop.create_task(server.start())
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
def stop_wsgi_server(event): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server)
"""Stop the WSGI server."""
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server) hass.http = server
hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0' hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
else util.get_local_ip(), else util.get_local_ip(),
api_password, server_port, api_password, server_port,
@ -152,105 +164,84 @@ def setup(hass, config):
return True return True
def request_class(): class GzipFileSender(FileSender):
"""Generate request class. """FileSender class capable of sending gzip version if available."""
Done in method because of imports. # pylint: disable=invalid-name, too-few-public-methods
"""
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import BaseRequest, AcceptMixin
from werkzeug.utils import cached_property
class Request(BaseRequest, AcceptMixin): development = False
"""Base class for incoming requests."""
@cached_property @asyncio.coroutine
def json(self): def send(self, request, filepath):
"""Get the result of json.loads if possible.""" """Send filepath to client using request."""
if not self.data: gzip = False
return None if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
# elif 'json' not in self.environ.get('CONTENT_TYPE', ''): gzip_path = filepath.with_name(filepath.name + '.gz')
# raise BadRequest('Not a JSON request')
try:
return json.loads(self.data.decode(
self.charset, self.encoding_errors))
except (TypeError, ValueError):
raise BadRequest('Unable to read JSON request')
return Request if gzip_path.is_file():
filepath = gzip_path
gzip = True
st = filepath.stat()
modsince = request.if_modified_since
if modsince is not None and st.st_mtime <= modsince.timestamp():
raise HTTPNotModified()
ct, encoding = mimetypes.guess_type(str(filepath))
if not ct:
ct = 'application/octet-stream'
resp = self._response_factory()
resp.content_type = ct
if encoding:
resp.headers[hdrs.CONTENT_ENCODING] = encoding
if gzip:
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
resp.last_modified = st.st_mtime
# CACHE HACK
if not self.development:
cache_time = 31 * 86400 # = 1 month
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time)
file_size = st.st_size
resp.content_length = file_size
resp.set_tcp_cork(True)
try:
with filepath.open('rb') as f:
yield from self._sendfile(request, resp, f, file_size)
finally:
resp.set_tcp_nodelay(True)
return resp
_GZIP_FILE_SENDER = GzipFileSender()
def routing_map(hass): class HAStaticRoute(StaticRoute):
"""Generate empty routing map with HA validators.""" """StaticRoute with support for fingerprinting."""
from werkzeug.routing import Map, BaseConverter, ValidationError
class EntityValidator(BaseConverter): def __init__(self, prefix, path):
"""Validate entity_id in urls.""" """Initialize a static route with gzip and cache busting support."""
super().__init__(None, prefix, path)
self._file_sender = _GZIP_FILE_SENDER
regex = r"(\w+)\.(\w+)" def match(self, path):
"""Match path to filename."""
if not path.startswith(self._prefix):
return None
def __init__(self, url_map, exist=True, domain=None): # Extra sauce to remove fingerprinted resource names
"""Initilalize entity validator.""" filename = path[self._prefix_len:]
super().__init__(url_map) fingerprinted = _FINGERPRINT.match(filename)
self._exist = exist if fingerprinted:
self._domain = domain filename = '{}.{}'.format(*fingerprinted.groups())
def to_python(self, value): return {'filename': filename}
"""Validate entity id."""
if self._exist and hass.states.get(value) is None:
raise ValidationError()
if self._domain is not None and \
split_entity_id(value)[0] != self._domain:
raise ValidationError()
return value
def to_url(self, value):
"""Convert entity_id for a url."""
return value
class DateValidator(BaseConverter):
"""Validate dates in urls."""
regex = r'\d{4}-\d{1,2}-\d{1,2}'
def to_python(self, value):
"""Validate and convert date."""
parsed = dt_util.parse_date(value)
if parsed is None:
raise ValidationError()
return parsed
def to_url(self, value):
"""Convert date to url value."""
return value.isoformat()
class DateTimeValidator(BaseConverter):
"""Validate datetimes in urls formatted per ISO 8601."""
regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \
r'\.\d+([+-][0-2]\d:[0-5]\d|Z)'
def to_python(self, value):
"""Validate and convert date."""
parsed = dt_util.parse_datetime(value)
if parsed is None:
raise ValidationError()
return parsed
def to_url(self, value):
"""Convert date to url value."""
return value.isoformat()
return Map(converters={
'entity': EntityValidator,
'date': DateValidator,
'datetime': DateTimeValidator,
})
class HomeAssistantWSGI(object): class HomeAssistantWSGI(object):
@ -262,28 +253,35 @@ class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate, def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins, ssl_key, server_host, server_port, cors_origins,
trusted_networks): trusted_networks):
"""Initilalize the WSGI Home Assistant server.""" """Initialize the WSGI Home Assistant server."""
from werkzeug.wrappers import Response import aiohttp_cors
Response.mimetype = 'text/html' self.app = web.Application(loop=hass.loop)
# pylint: disable=invalid-name
self.Request = request_class()
self.url_map = routing_map(hass)
self.views = {}
self.hass = hass self.hass = hass
self.extra_apps = {}
self.development = development self.development = development
self.api_password = api_password self.api_password = api_password
self.ssl_certificate = ssl_certificate self.ssl_certificate = ssl_certificate
self.ssl_key = ssl_key self.ssl_key = ssl_key
self.server_host = server_host self.server_host = server_host
self.server_port = server_port self.server_port = server_port
self.cors_origins = cors_origins
self.trusted_networks = trusted_networks self.trusted_networks = trusted_networks
self.event_forwarder = None self.event_forwarder = None
self._handler = None
self.server = None self.server = None
if cors_origins:
self.cors = aiohttp_cors.setup(self.app, defaults={
host: aiohttp_cors.ResourceOptions(
allow_headers=ALLOWED_CORS_HEADERS,
allow_methods='*',
) for host in cors_origins
})
else:
self.cors = None
# CACHE HACK
_GZIP_FILE_SENDER.development = development
def register_view(self, view): def register_view(self, view):
"""Register a view with the WSGI server. """Register a view with the WSGI server.
@ -291,21 +289,11 @@ class HomeAssistantWSGI(object):
It is optional to instantiate it before registering; this method will It is optional to instantiate it before registering; this method will
handle it either way. handle it either way.
""" """
from werkzeug.routing import Rule
if view.name in self.views:
_LOGGER.warning("View '%s' is being overwritten", view.name)
if isinstance(view, type): if isinstance(view, type):
# Instantiate the view, if needed # Instantiate the view, if needed
view = view(self.hass) view = view(self.hass)
self.views[view.name] = view view.register(self.app.router)
rule = Rule(view.url, endpoint=view.name)
self.url_map.add(rule)
for url in view.extra_urls:
rule = Rule(url, endpoint=view.name)
self.url_map.add(rule)
def register_redirect(self, url, redirect_to): def register_redirect(self, url, redirect_to):
"""Register a redirect with the server. """Register a redirect with the server.
@ -316,149 +304,92 @@ class HomeAssistantWSGI(object):
for the redirect, otherwise it has to be a string with placeholders in for the redirect, otherwise it has to be a string with placeholders in
rule syntax. rule syntax.
""" """
from werkzeug.routing import Rule def redirect(request):
"""Redirect to location."""
raise HTTPMovedPermanently(redirect_to)
self.url_map.add(Rule(url, redirect_to=redirect_to)) self.app.router.add_route('GET', url, redirect)
def register_static_path(self, url_root, path, cache_length=31): def register_static_path(self, url_root, path, cache_length=31):
"""Register a folder to serve as a static path. """Register a folder to serve as a static path.
Specify optional cache length of asset in days. Specify optional cache length of asset in days.
""" """
from static import Cling if os.path.isdir(path):
assert url_root.startswith('/')
if not url_root.endswith('/'):
url_root += '/'
route = HAStaticRoute(url_root, path)
self.app.router.register_route(route)
return
headers = [] filepath = Path(path)
if cache_length and not self.development: @asyncio.coroutine
# 1 year in seconds def serve_file(request):
cache_time = cache_length * 86400 """Redirect to location."""
return _GZIP_FILE_SENDER.send(request, filepath)
headers.append({ # aiohttp supports regex matching for variables. Using that as temp
'prefix': '', # to work around cache busting MD5.
HTTP_HEADER_CACHE_CONTROL: # Turns something like /static/dev-panel.html into
"public, max-age={}".format(cache_time) # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
}) base, ext = url_root.rsplit('.', 1)
base, file = base.rsplit('/', 1)
regex = r"{}(-[a-z0-9]{{32}}|)\.{}".format(file, ext)
url_pattern = "{}/{{filename:{}}}".format(base, regex)
self.register_wsgi_app(url_root, Cling(path, headers=headers)) self.app.router.add_route('GET', url_pattern, serve_file)
def register_wsgi_app(self, url_root, app):
"""Register a path to serve a WSGI app."""
if url_root in self.extra_apps:
_LOGGER.warning("Url root '%s' is being overwritten", url_root)
self.extra_apps[url_root] = app
@asyncio.coroutine
def start(self): def start(self):
"""Start the wsgi server.""" """Start the wsgi server."""
from cherrypy import wsgiserver if self.cors is not None:
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter for route in list(self.app.router.routes()):
self.cors.add(route)
# pylint: disable=too-few-public-methods,super-init-not-called
class ContextSSLAdapter(BuiltinSSLAdapter):
"""SSL Adapter that takes in an SSL context."""
def __init__(self, context):
self.context = context
# pylint: disable=no-member
self.server = wsgiserver.CherryPyWSGIServer(
(self.server_host, self.server_port), self,
server_name='Home Assistant')
if self.ssl_certificate: if self.ssl_certificate:
context = ssl.SSLContext(SSL_VERSION) context = ssl.SSLContext(SSL_VERSION)
context.options |= SSL_OPTS context.options |= SSL_OPTS
context.set_ciphers(CIPHERS) context.set_ciphers(CIPHERS)
context.load_cert_chain(self.ssl_certificate, self.ssl_key) context.load_cert_chain(self.ssl_certificate, self.ssl_key)
self.server.ssl_adapter = ContextSSLAdapter(context) else:
context = None
threading.Thread( self._handler = self.app.make_handler()
target=self.server.start, daemon=True, name='WSGI-server').start() self.server = yield from self.hass.loop.create_server(
self._handler, self.server_host, self.server_port, ssl=context)
@asyncio.coroutine
def stop(self): def stop(self):
"""Stop the wsgi server.""" """Stop the wsgi server."""
self.server.stop() self.server.close()
yield from self.server.wait_closed()
def dispatch_request(self, request): yield from self.app.shutdown()
"""Handle incoming request.""" yield from self._handler.finish_connections(60.0)
from werkzeug.exceptions import ( yield from self.app.cleanup()
MethodNotAllowed, NotFound, BadRequest, Unauthorized,
)
from werkzeug.routing import RequestRedirect
with request:
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return self.views[endpoint].handle_request(request, **values)
except RequestRedirect as ex:
return ex
except (BadRequest, NotFound, MethodNotAllowed,
Unauthorized) as ex:
resp = ex.get_response(request.environ)
if request.accept_mimetypes.accept_json:
resp.data = json.dumps({
'result': 'error',
'message': str(ex),
})
resp.mimetype = CONTENT_TYPE_JSON
return resp
def base_app(self, environ, start_response):
"""WSGI Handler of requests to base app."""
request = self.Request(environ)
response = self.dispatch_request(request)
if self.cors_origins:
cors_check = (environ.get('HTTP_ORIGIN') in self.cors_origins)
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
if cors_check:
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
environ.get('HTTP_ORIGIN')
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
cors_headers
return response(environ, start_response)
def __call__(self, environ, start_response):
"""Handle a request for base app + extra apps."""
from werkzeug.wsgi import DispatcherMiddleware
if not self.hass.is_running:
from werkzeug.exceptions import BadRequest
return BadRequest()(environ, start_response)
app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
if fingerprinted:
environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
return app(environ, start_response)
@staticmethod @staticmethod
def get_real_ip(request): def get_real_ip(request):
"""Return the clients correct ip address, even in proxied setups.""" """Return the clients correct ip address, even in proxied setups."""
if request.access_route: peername = request.transport.get_extra_info('peername')
return request.access_route[-1] return peername[0] if peername is not None else None
else:
return request.remote_addr
def is_trusted_ip(self, remote_addr): def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks.""" """Match an ip address against trusted CIDR networks."""
return any(ip_address(remote_addr) in trusted_network return any(ip_address(remote_addr) in trusted_network
for trusted_network in self.hass.wsgi.trusted_networks) for trusted_network in self.hass.http.trusted_networks)
class HomeAssistantView(object): class HomeAssistantView(object):
"""Base view for all views.""" """Base view for all views."""
url = None
extra_urls = [] extra_urls = []
requires_auth = True # Views inheriting from this class can override this requires_auth = True # Views inheriting from this class can override this
def __init__(self, hass): def __init__(self, hass):
"""Initilalize the base view.""" """Initilalize the base view."""
from werkzeug.wrappers import Response
if not hasattr(self, 'url'): if not hasattr(self, 'url'):
class_name = self.__class__.__name__ class_name = self.__class__.__name__
raise AttributeError( raise AttributeError(
@ -472,59 +403,99 @@ class HomeAssistantView(object):
) )
self.hass = hass self.hass = hass
# pylint: disable=invalid-name
self.Response = Response
def handle_request(self, request, **values): def json(self, result, status_code=200): # pylint: disable=no-self-use
"""Handle request to url.""" """Return a JSON response."""
from werkzeug.exceptions import MethodNotAllowed, Unauthorized msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
return web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code)
if request.method == "OPTIONS": def json_message(self, error, status_code=200):
# For CORS preflight requests. """Return a JSON message response."""
return self.options(request) return self.json({'message': error}, status_code)
try: @asyncio.coroutine
handler = getattr(self, request.method.lower()) def file(self, request, fil): # pylint: disable=no-self-use
except AttributeError: """Return a file."""
raise MethodNotAllowed assert isinstance(fil, str), 'only string paths allowed'
response = yield from _GZIP_FILE_SENDER.send(request, Path(fil))
return response
def register(self, router):
"""Register the view with a router."""
assert self.url is not None, 'No url set for view'
urls = [self.url] + self.extra_urls
for method in ('get', 'post', 'delete', 'put'):
handler = getattr(self, method, None)
if not handler:
continue
handler = request_handler_factory(self, handler)
for url in urls:
router.add_route(method, url, handler)
# aiohttp_cors does not work with class based views
# self.app.router.add_route('*', self.url, self, name=self.name)
# for url in self.extra_urls:
# self.app.router.add_route('*', url, self)
def request_handler_factory(view, handler):
"""Factory to wrap our handler classes.
Eventually authentication should be managed by middleware.
"""
@asyncio.coroutine
def handle(request):
"""Handle incoming request."""
remote_addr = HomeAssistantWSGI.get_real_ip(request) remote_addr = HomeAssistantWSGI.get_real_ip(request)
# Auth code verbose on purpose # Auth code verbose on purpose
authenticated = False authenticated = False
if self.hass.wsgi.api_password is None: if view.hass.http.api_password is None:
authenticated = True authenticated = True
elif self.hass.wsgi.is_trusted_ip(remote_addr): elif view.hass.http.is_trusted_ip(remote_addr):
authenticated = True authenticated = True
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''), elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
self.hass.wsgi.api_password): view.hass.http.api_password):
# A valid auth header has been set # A valid auth header has been set
authenticated = True authenticated = True
elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''), elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
self.hass.wsgi.api_password): view.hass.http.api_password):
authenticated = True authenticated = True
if self.requires_auth and not authenticated: if view.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid ' _LOGGER.warning('Login attempt or request with an invalid '
'password from %s', remote_addr) 'password from %s', remote_addr)
persistent_notification.create( persistent_notification.async_create(
self.hass, view.hass,
'Invalid password used from {}'.format(remote_addr), 'Invalid password used from {}'.format(remote_addr),
'Login attempt failed', NOTIFICATION_ID_LOGIN) 'Login attempt failed', NOTIFICATION_ID_LOGIN)
raise Unauthorized() raise HTTPUnauthorized()
request.authenticated = authenticated request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)', _LOGGER.info('Serving %s to %s (auth: %s)',
request.path, remote_addr, authenticated) request.path, remote_addr, authenticated)
result = handler(request, **values) assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
"Handler should be a coroutine or a callback."
if isinstance(result, self.Response): result = handler(request, **request.match_info)
if asyncio.iscoroutine(result):
result = yield from result
if isinstance(result, web.StreamResponse):
# The method handler returned a ready-made Response, how nice of it # The method handler returned a ready-made Response, how nice of it
return result return result
@ -533,36 +504,14 @@ class HomeAssistantView(object):
if isinstance(result, tuple): if isinstance(result, tuple):
result, status_code = result result, status_code = result
return self.Response(result, status=status_code) if isinstance(result, str):
result = result.encode('utf-8')
elif result is None:
result = b''
elif not isinstance(result, bytes):
assert False, ('Result should be None, string, bytes or Response. '
'Got: {}').format(result)
def json(self, result, status_code=200): return web.Response(body=result, status=status_code)
"""Return a JSON response."""
msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
return self.Response(
msg, mimetype=CONTENT_TYPE_JSON, status=status_code)
def json_message(self, error, status_code=200): return handle
"""Return a JSON message response."""
return self.json({'message': error}, status_code)
def file(self, request, fil, mimetype=None):
"""Return a file."""
from werkzeug.wsgi import wrap_file
from werkzeug.exceptions import NotFound
if isinstance(fil, str):
if mimetype is None:
mimetype = mimetypes.guess_type(fil)[0]
try:
fil = open(fil, mode='br')
except IOError:
raise NotFound()
return self.Response(wrap_file(request.environ, fil),
mimetype=mimetype, direct_passthrough=True)
def options(self, request):
"""Default handler for OPTIONS (necessary for CORS preflight)."""
return self.Response('', status=200)

View file

@ -247,7 +247,7 @@ def setup(hass, config):
discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
hass.wsgi.register_view(iOSIdentifyDeviceView(hass)) hass.http.register_view(iOSIdentifyDeviceView(hass))
app_config = config.get(DOMAIN, {}) app_config = config.get(DOMAIN, {})
hass.wsgi.register_view(iOSPushConfigView(hass, hass.wsgi.register_view(iOSPushConfigView(hass,

View file

@ -11,6 +11,7 @@ from itertools import groupby
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun from homeassistant.components import recorder, sun
@ -19,7 +20,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START, from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_NOT_HOME, STATE_OFF, STATE_ON,
ATTR_HIDDEN) ATTR_HIDDEN, HTTP_BAD_REQUEST)
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async import run_callback_threadsafe
@ -88,7 +89,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None):
def setup(hass, config): def setup(hass, config):
"""Listen for download events to download files.""" """Listen for download events to download files."""
@asyncio.coroutine @callback
def log_message(service): def log_message(service):
"""Handle sending notification message service calls.""" """Handle sending notification message service calls."""
message = service.data[ATTR_MESSAGE] message = service.data[ATTR_MESSAGE]
@ -100,7 +101,7 @@ def setup(hass, config):
message = message.async_render() message = message.async_render()
async_log_entry(hass, name, message, domain, entity_id) async_log_entry(hass, name, message, domain, entity_id)
hass.wsgi.register_view(LogbookView(hass, config)) hass.http.register_view(LogbookView(hass, config))
register_built_in_panel(hass, 'logbook', 'Logbook', register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type') 'mdi:format-list-bulleted-type')
@ -115,24 +116,37 @@ class LogbookView(HomeAssistantView):
url = '/api/logbook' url = '/api/logbook'
name = 'api:logbook' name = 'api:logbook'
extra_urls = ['/api/logbook/<datetime:datetime>'] extra_urls = ['/api/logbook/{datetime}']
def __init__(self, hass, config): def __init__(self, hass, config):
"""Initilalize the logbook view.""" """Initilalize the logbook view."""
super().__init__(hass) super().__init__(hass)
self.config = config self.config = config
@asyncio.coroutine
def get(self, request, datetime=None): def get(self, request, datetime=None):
"""Retrieve logbook entries.""" """Retrieve logbook entries."""
start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day()) if datetime:
datetime = dt_util.parse_datetime(datetime)
if datetime is None:
return self.json_message('Invalid datetime', HTTP_BAD_REQUEST)
else:
datetime = dt_util.start_of_local_day()
start_day = dt_util.as_utc(datetime)
end_day = start_day + timedelta(days=1) end_day = start_day + timedelta(days=1)
events = recorder.get_model('Events') def get_results():
query = recorder.query('Events').filter( """Query DB for results."""
(events.time_fired > start_day) & events = recorder.get_model('Events')
(events.time_fired < end_day)) query = recorder.query('Events').filter(
events = recorder.execute(query) (events.time_fired > start_day) &
events = _exclude_events(events, self.config) (events.time_fired < end_day))
events = recorder.execute(query)
return _exclude_events(events, self.config)
events = yield from self.hass.loop.run_in_executor(None, get_results)
return self.json(humanify(events)) return self.json(humanify(events))

View file

@ -4,11 +4,13 @@ Component to interface with various media players.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/media_player/ https://home-assistant.io/components/media_player/
""" """
import asyncio
import hashlib import hashlib
import logging import logging
import os import os
import requests import requests
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
@ -291,7 +293,7 @@ def setup(hass, config):
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(MediaPlayerImageView(hass, component.entities)) hass.http.register_view(MediaPlayerImageView(hass, component.entities))
component.setup(config) component.setup(config)
@ -677,7 +679,7 @@ class MediaPlayerImageView(HomeAssistantView):
"""Media player view to serve an image.""" """Media player view to serve an image."""
requires_auth = False requires_auth = False
url = "/api/media_player_proxy/<entity(domain=media_player):entity_id>" url = "/api/media_player_proxy/{entity_id}"
name = "api:media_player:image" name = "api:media_player:image"
def __init__(self, hass, entities): def __init__(self, hass, entities):
@ -685,26 +687,34 @@ class MediaPlayerImageView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.entities = entities self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id): def get(self, request, entity_id):
"""Start a get request.""" """Start a get request."""
player = self.entities.get(entity_id) player = self.entities.get(entity_id)
if player is None: if player is None:
return self.Response(status=404) return web.Response(status=404)
authenticated = (request.authenticated or authenticated = (request.authenticated or
request.args.get('token') == player.access_token) request.GET.get('token') == player.access_token)
if not authenticated: if not authenticated:
return self.Response(status=401) return web.Response(status=401)
image_url = player.media_image_url image_url = player.media_image_url
if image_url:
response = requests.get(image_url) if image_url is None:
else: return web.Response(status=404)
response = None
def fetch_image():
"""Helper method to fetch image."""
try:
return requests.get(image_url).content
except requests.RequestException:
return None
response = yield from self.hass.loop.run_in_executor(None, fetch_image)
if response is None: if response is None:
return self.Response(status=500) return web.Response(status=500)
return self.Response(response) return web.Response(body=response)

View file

@ -4,6 +4,7 @@ HTML5 Push Messaging notification service.
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/notify.html5/ https://home-assistant.io/components/notify.html5/
""" """
import asyncio
import os import os
import logging import logging
import json import json
@ -107,9 +108,9 @@ def get_service(hass, config):
if registrations is None: if registrations is None:
return None return None
hass.wsgi.register_view( hass.http.register_view(
HTML5PushRegistrationView(hass, registrations, json_path)) HTML5PushRegistrationView(hass, registrations, json_path))
hass.wsgi.register_view(HTML5PushCallbackView(hass, registrations)) hass.http.register_view(HTML5PushCallbackView(hass, registrations))
gcm_api_key = config.get(ATTR_GCM_API_KEY) gcm_api_key = config.get(ATTR_GCM_API_KEY)
gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) gcm_sender_id = config.get(ATTR_GCM_SENDER_ID)
@ -163,12 +164,18 @@ class HTML5PushRegistrationView(HomeAssistantView):
self.registrations = registrations self.registrations = registrations
self.json_path = json_path self.json_path = json_path
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Accept the POST request for push registrations from a browser.""" """Accept the POST request for push registrations from a browser."""
try: try:
data = REGISTER_SCHEMA(request.json) data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
try:
data = REGISTER_SCHEMA(data)
except vol.Invalid as ex: except vol.Invalid as ex:
return self.json_message(humanize_error(request.json, ex), return self.json_message(humanize_error(data, ex),
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
name = ensure_unique_string('unnamed device', name = ensure_unique_string('unnamed device',
@ -182,9 +189,15 @@ class HTML5PushRegistrationView(HomeAssistantView):
return self.json_message('Push notification subscriber registered.') return self.json_message('Push notification subscriber registered.')
@asyncio.coroutine
def delete(self, request): def delete(self, request):
"""Delete a registration.""" """Delete a registration."""
subscription = request.json.get(ATTR_SUBSCRIPTION) try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
subscription = data.get(ATTR_SUBSCRIPTION)
found = None found = None
@ -270,23 +283,29 @@ class HTML5PushCallbackView(HomeAssistantView):
status_code=HTTP_UNAUTHORIZED) status_code=HTTP_UNAUTHORIZED)
return payload return payload
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Accept the POST request for push registrations event callback.""" """Accept the POST request for push registrations event callback."""
auth_check = self.check_authorization_header(request) auth_check = self.check_authorization_header(request)
if not isinstance(auth_check, dict): if not isinstance(auth_check, dict):
return auth_check return auth_check
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
event_payload = { event_payload = {
ATTR_TAG: request.json.get(ATTR_TAG), ATTR_TAG: data.get(ATTR_TAG),
ATTR_TYPE: request.json[ATTR_TYPE], ATTR_TYPE: data[ATTR_TYPE],
ATTR_TARGET: auth_check[ATTR_TARGET], ATTR_TARGET: auth_check[ATTR_TARGET],
} }
if request.json.get(ATTR_ACTION) is not None: if data.get(ATTR_ACTION) is not None:
event_payload[ATTR_ACTION] = request.json.get(ATTR_ACTION) event_payload[ATTR_ACTION] = data.get(ATTR_ACTION)
if request.json.get(ATTR_DATA) is not None: if data.get(ATTR_DATA) is not None:
event_payload[ATTR_DATA] = request.json.get(ATTR_DATA) event_payload[ATTR_DATA] = data.get(ATTR_DATA)
try: try:
event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload) event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload)

View file

@ -153,7 +153,7 @@ def setup(hass, config):
# Create Alpr device / render engine # Create Alpr device / render engine
if render == RENDER_FFMPEG: if render == RENDER_FFMPEG:
use_render_fffmpeg = True use_render_fffmpeg = True
if not run_test(input_source): if not run_test(hass, input_source):
_LOGGER.error("'%s' is not valid ffmpeg input", input_source) _LOGGER.error("'%s' is not valid ffmpeg input", input_source)
continue continue

View file

@ -4,6 +4,7 @@ A component which is collecting configuration errors.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/persistent_notification/ https://home-assistant.io/components/persistent_notification/
""" """
import asyncio
import os import os
import logging import logging
@ -14,6 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'persistent_notification' DOMAIN = 'persistent_notification'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -35,6 +37,14 @@ _LOGGER = logging.getLogger(__name__)
def create(hass, message, title=None, notification_id=None): def create(hass, message, title=None, notification_id=None):
"""Generate a notification."""
run_coroutine_threadsafe(
async_create(hass, message, title, notification_id), hass.loop
).result()
@asyncio.coroutine
def async_create(hass, message, title=None, notification_id=None):
"""Generate a notification.""" """Generate a notification."""
data = { data = {
key: value for key, value in [ key: value for key, value in [
@ -44,7 +54,7 @@ def create(hass, message, title=None, notification_id=None):
] if value is not None ] if value is not None
} }
hass.services.call(DOMAIN, SERVICE_CREATE, data) yield from hass.services.async_call(DOMAIN, SERVICE_CREATE, data)
def setup(hass, config): def setup(hass, config):

View file

@ -12,6 +12,7 @@ import time
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -273,8 +274,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
scope=['activity', 'heartrate', 'nutrition', 'profile', scope=['activity', 'heartrate', 'nutrition', 'profile',
'settings', 'sleep', 'weight']) 'settings', 'sleep', 'weight'])
hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url) hass.http.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
hass.wsgi.register_view(FitbitAuthCallbackView( hass.http.register_view(FitbitAuthCallbackView(
hass, config, add_devices, oauth)) hass, config, add_devices, oauth))
request_oauth_completion(hass) request_oauth_completion(hass)
@ -294,12 +295,13 @@ class FitbitAuthCallbackView(HomeAssistantView):
self.add_devices = add_devices self.add_devices = add_devices
self.oauth = oauth self.oauth = oauth
@callback
def get(self, request): def get(self, request):
"""Finish OAuth callback request.""" """Finish OAuth callback request."""
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
from oauthlib.oauth2.rfc6749.errors import MissingTokenError from oauthlib.oauth2.rfc6749.errors import MissingTokenError
data = request.args data = request.GET
response_message = """Fitbit has been successfully authorized! response_message = """Fitbit has been successfully authorized!
You can close this window now!""" You can close this window now!"""
@ -340,7 +342,8 @@ class FitbitAuthCallbackView(HomeAssistantView):
config_contents): config_contents):
_LOGGER.error("Failed to save config file") _LOGGER.error("Failed to save config file")
setup_platform(self.hass, self.config, self.add_devices) self.hass.async_add_job(setup_platform, self.hass, self.config,
self.add_devices)
return html_response return html_response

View file

@ -9,6 +9,7 @@ import re
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_EMAIL, CONF_NAME) from homeassistant.const import (CONF_EMAIL, CONF_NAME)
@ -57,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
email = config.get(CONF_EMAIL) email = config.get(CONF_EMAIL)
sensors = {} sensors = {}
hass.wsgi.register_view(TorqueReceiveDataView( hass.http.register_view(TorqueReceiveDataView(
hass, email, vehicle, sensors, add_devices)) hass, email, vehicle, sensors, add_devices))
return True return True
@ -77,9 +78,10 @@ class TorqueReceiveDataView(HomeAssistantView):
self.sensors = sensors self.sensors = sensors
self.add_devices = add_devices self.add_devices = add_devices
@callback
def get(self, request): def get(self, request):
"""Handle Torque data request.""" """Handle Torque data request."""
data = request.args data = request.GET
if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]:
return return
@ -100,14 +102,14 @@ class TorqueReceiveDataView(HomeAssistantView):
elif is_value: elif is_value:
pid = convert_pid(is_value.group(1)) pid = convert_pid(is_value.group(1))
if pid in self.sensors: if pid in self.sensors:
self.sensors[pid].on_update(data[key]) self.sensors[pid].async_on_update(data[key])
for pid in names: for pid in names:
if pid not in self.sensors: if pid not in self.sensors:
self.sensors[pid] = TorqueSensor( self.sensors[pid] = TorqueSensor(
ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]), ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]),
units.get(pid, None)) units.get(pid, None))
self.add_devices([self.sensors[pid]]) self.hass.async_add_job(self.add_devices, [self.sensors[pid]])
return None return None
@ -141,7 +143,8 @@ class TorqueSensor(Entity):
"""Return the default icon of the sensor.""" """Return the default icon of the sensor."""
return 'mdi:car' return 'mdi:car'
def on_update(self, value): @callback
def async_on_update(self, value):
"""Receive an update.""" """Receive an update."""
self._state = value self._state = value
self.update_ha_state() self.hass.loop.create_task(self.async_update_ha_state())

View file

@ -10,6 +10,7 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant import util from homeassistant import util
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ( from homeassistant.const import (
@ -40,7 +41,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
REQ_CONF = [CONF_HOST, CONF_OUTLETS] REQ_CONF = [CONF_HOST, CONF_OUTLETS]
URL_API_NETIO_EP = '/api/netio/<host>' URL_API_NETIO_EP = '/api/netio/{host}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
if len(DEVICES) == 0: if len(DEVICES) == 0:
hass.wsgi.register_view(NetioApiView) hass.http.register_view(NetioApiView)
dev = Netio(host, port, username, password) dev = Netio(host, port, username, password)
@ -93,9 +94,10 @@ class NetioApiView(HomeAssistantView):
url = URL_API_NETIO_EP url = URL_API_NETIO_EP
name = 'api:netio' name = 'api:netio'
@callback
def get(self, request, host): def get(self, request, host):
"""Request handler.""" """Request handler."""
data = request.args data = request.GET
states, consumptions, cumulated_consumptions, start_dates = \ states, consumptions, cumulated_consumptions, start_dates = \
[], [], [], [] [], [], [], []
@ -117,7 +119,7 @@ class NetioApiView(HomeAssistantView):
ndev.start_dates = start_dates ndev.start_dates = start_dates
for dev in DEVICES[host].entities: for dev in DEVICES[host].entities:
dev.update_ha_state() self.hass.loop.create_task(dev.async_update_ha_state())
return self.json(True) return self.json(True)

View file

@ -83,12 +83,14 @@ SERVICE_TO_STATE = {
# pylint: disable=too-few-public-methods, attribute-defined-outside-init # pylint: disable=too-few-public-methods, attribute-defined-outside-init
class TrackStates(object): class AsyncTrackStates(object):
""" """
Record the time when the with-block is entered. Record the time when the with-block is entered.
Add all states that have changed since the start time to the return list Add all states that have changed since the start time to the return list
when with-block is exited. when with-block is exited.
Must be run within the event loop.
""" """
def __init__(self, hass): def __init__(self, hass):
@ -103,7 +105,8 @@ class TrackStates(object):
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
"""Add changes states to changes list.""" """Add changes states to changes list."""
self.states.extend(get_changed_since(self.hass.states.all(), self.now)) self.states.extend(get_changed_since(self.hass.states.async_all(),
self.now))
def get_changed_since(states, utc_point_in_time): def get_changed_since(states, utc_point_in_time):

View file

@ -213,35 +213,35 @@ class EventForwarder(object):
self._targets = {} self._targets = {}
self._lock = threading.Lock() self._lock = threading.Lock()
self._unsub_listener = None self._async_unsub_listener = None
def connect(self, api): @ha.callback
def async_connect(self, api):
"""Attach to a Home Assistant instance and forward events. """Attach to a Home Assistant instance and forward events.
Will overwrite old target if one exists with same host/port. Will overwrite old target if one exists with same host/port.
""" """
with self._lock: if self._async_unsub_listener is None:
if self._unsub_listener is None: self._async_unsub_listener = self.hass.bus.async_listen(
self._unsub_listener = self.hass.bus.listen( ha.MATCH_ALL, self._event_listener)
ha.MATCH_ALL, self._event_listener)
key = (api.host, api.port) key = (api.host, api.port)
self._targets[key] = api self._targets[key] = api
def disconnect(self, api): @ha.callback
def async_disconnect(self, api):
"""Remove target from being forwarded to.""" """Remove target from being forwarded to."""
with self._lock: key = (api.host, api.port)
key = (api.host, api.port)
did_remove = self._targets.pop(key, None) is None did_remove = self._targets.pop(key, None) is None
if len(self._targets) == 0: if len(self._targets) == 0:
# Remove event listener if no forwarding targets present # Remove event listener if no forwarding targets present
self._unsub_listener() self._async_unsub_listener()
self._unsub_listener = None self._async_unsub_listener = None
return did_remove return did_remove
def _event_listener(self, event): def _event_listener(self, event):
"""Listen and forward all events.""" """Listen and forward all events."""

View file

@ -6,6 +6,8 @@ pip>=7.0.0
jinja2>=2.8 jinja2>=2.8
voluptuous==0.9.2 voluptuous==0.9.2
typing>=3,<4 typing>=3,<4
aiohttp==1.0.5
async_timeout==1.0.0
# homeassistant.components.nuimo_controller # homeassistant.components.nuimo_controller
--only-binary=all git+https://github.com/getSenic/nuimo-linux-python#nuimo==1.0.0 --only-binary=all git+https://github.com/getSenic/nuimo-linux-python#nuimo==1.0.0
@ -28,9 +30,8 @@ SoCo==0.12
# homeassistant.components.notify.twitter # homeassistant.components.notify.twitter
TwitterAPI==2.4.2 TwitterAPI==2.4.2
# homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
Werkzeug==0.11.11 aiohttp_cors==0.4.0
# homeassistant.components.apcupsd # homeassistant.components.apcupsd
apcaccess==0.0.4 apcaccess==0.0.4
@ -62,10 +63,6 @@ blockchain==1.3.3
# homeassistant.components.notify.aws_sqs # homeassistant.components.notify.aws_sqs
boto3==1.3.1 boto3==1.3.1
# homeassistant.components.emulated_hue
# homeassistant.components.http
cherrypy==8.1.2
# homeassistant.components.sensor.coinmarketcap # homeassistant.components.sensor.coinmarketcap
coinmarketcap==2.0.1 coinmarketcap==2.0.1
@ -136,7 +133,7 @@ gps3==0.33.3
ha-alpr==0.3 ha-alpr==0.3
# homeassistant.components.ffmpeg # homeassistant.components.ffmpeg
ha-ffmpeg==0.13 ha-ffmpeg==0.14
# homeassistant.components.mqtt.server # homeassistant.components.mqtt.server
hbmqtt==0.7.1 hbmqtt==0.7.1
@ -483,10 +480,6 @@ speedtest-cli==0.3.4
# homeassistant.scripts.db_migrator # homeassistant.scripts.db_migrator
sqlalchemy==1.1.1 sqlalchemy==1.1.1
# homeassistant.components.emulated_hue
# homeassistant.components.http
static3==0.7.0
# homeassistant.components.statsd # homeassistant.components.statsd
statsd==3.2.1 statsd==3.2.1

View file

@ -2,6 +2,7 @@ flake8>=3.0.4
pylint>=1.5.6 pylint>=1.5.6
coveralls>=1.1 coveralls>=1.1
pytest>=2.9.2 pytest>=2.9.2
pytest-aiohttp>=0.1.3
pytest-asyncio>=0.5.0 pytest-asyncio>=0.5.0
pytest-cov>=2.3.1 pytest-cov>=2.3.1
pytest-timeout>=1.0.0 pytest-timeout>=1.0.0
@ -9,3 +10,4 @@ pytest-catchlog>=1.2.2
pydocstyle>=1.0.0 pydocstyle>=1.0.0
requests_mock>=1.0 requests_mock>=1.0
mypy-lang>=0.4 mypy-lang>=0.4
mock-open>=1.3.1

View file

@ -21,6 +21,8 @@ REQUIRES = [
'jinja2>=2.8', 'jinja2>=2.8',
'voluptuous==0.9.2', 'voluptuous==0.9.2',
'typing>=3,<4', 'typing>=3,<4',
'aiohttp==1.0.5',
'async_timeout==1.0.0',
] ]
setup( setup(

View file

@ -1,29 +1 @@
"""Setup some common test helper things.""" """Tests for Home Assistant."""
import functools
import logging
from homeassistant import util
from homeassistant.util import location
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
def test_real(func):
"""Force a function to require a keyword _test_real to be passed in."""
@functools.wraps(func)
def guard_func(*args, **kwargs):
real = kwargs.pop('_test_real', None)
if not real:
raise Exception('Forgot to mock or pass "_test_real=True" to %s',
func.__name__)
return func(*args, **kwargs)
return guard_func
# Guard a few functions that would make network connections
location.detect_location_info = test_real(location.detect_location_info)
location.elevation = test_real(location.elevation)
util.get_local_ip = lambda: '127.0.0.1'

View file

@ -38,23 +38,11 @@ def get_test_home_assistant(num_threads=None):
orig_num_threads = ha.MIN_WORKER_THREAD orig_num_threads = ha.MIN_WORKER_THREAD
ha.MIN_WORKER_THREAD = num_threads ha.MIN_WORKER_THREAD = num_threads
hass = ha.HomeAssistant(loop) hass = loop.run_until_complete(async_test_home_assistant(loop))
if num_threads: if num_threads:
ha.MIN_WORKER_THREAD = orig_num_threads ha.MIN_WORKER_THREAD = orig_num_threads
hass.config.location_name = 'test home'
hass.config.config_dir = get_test_config_dir()
hass.config.latitude = 32.87336
hass.config.longitude = -117.22743
hass.config.elevation = 0
hass.config.time_zone = date_util.get_time_zone('US/Pacific')
hass.config.units = METRIC_SYSTEM
hass.config.skip_pip = True
if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS:
loader.prepare(hass)
# FIXME should not be a daemon. Means hass.stop() not called in teardown # FIXME should not be a daemon. Means hass.stop() not called in teardown
stop_event = threading.Event() stop_event = threading.Event()
@ -98,6 +86,35 @@ def get_test_home_assistant(num_threads=None):
return hass return hass
@asyncio.coroutine
def async_test_home_assistant(loop):
"""Return a Home Assistant object pointing at test config dir."""
loop._thread_ident = threading.get_ident()
def get_hass():
"""Temp while we migrate core HASS over to be async constructors."""
hass = ha.HomeAssistant(loop)
hass.config.location_name = 'test home'
hass.config.config_dir = get_test_config_dir()
hass.config.latitude = 32.87336
hass.config.longitude = -117.22743
hass.config.elevation = 0
hass.config.time_zone = date_util.get_time_zone('US/Pacific')
hass.config.units = METRIC_SYSTEM
hass.config.skip_pip = True
if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS:
loader.prepare(hass)
hass.state = ha.CoreState.running
return hass
hass = yield from loop.run_in_executor(None, get_hass)
return hass
def get_test_instance_port(): def get_test_instance_port():
"""Return unused port for running test instance. """Return unused port for running test instance.
@ -181,8 +198,19 @@ def mock_state_change_event(hass, new_state, old_state=None):
def mock_http_component(hass): def mock_http_component(hass):
"""Mock the HTTP component.""" """Mock the HTTP component."""
hass.wsgi = mock.MagicMock() hass.http = mock.MagicMock()
hass.config.components.append('http') hass.config.components.append('http')
hass.http.views = {}
def mock_register_view(view):
"""Store registered view."""
if isinstance(view, type):
# Instantiate the view, if needed
view = view(hass)
hass.http.views[view.name] = view
hass.http.register_view = mock_register_view
def mock_mqtt_component(hass): def mock_mqtt_component(hass):

View file

@ -1,36 +1,18 @@
"""The tests for generic camera component.""" """The tests for generic camera component."""
import unittest import asyncio
from unittest import mock from unittest import mock
import requests_mock
from werkzeug.test import EnvironBuilder
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import setup_component
from homeassistant.components.http import request_class
from tests.common import get_test_home_assistant
class TestGenericCamera(unittest.TestCase): @asyncio.coroutine
"""Test the generic camera platform.""" def test_fetching_url(aioclient_mock, hass, test_client):
"""Test that it fetches the given url."""
aioclient_mock.get('http://example.com', text='hello world')
def setUp(self): def setup_platform():
"""Setup things to be run when tests are started.""" """Setup the platform."""
self.hass = get_test_home_assistant() assert setup_component(hass, 'camera', {
self.hass.wsgi = mock.MagicMock()
self.hass.config.components.append('http')
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@requests_mock.Mocker()
def test_fetching_url(self, m):
"""Test that it fetches the given url."""
self.hass.wsgi = mock.MagicMock()
m.get('http://example.com', text='hello world')
assert setup_component(self.hass, 'camera', {
'camera': { 'camera': {
'name': 'config_test', 'name': 'config_test',
'platform': 'generic', 'platform': 'generic',
@ -39,32 +21,32 @@ class TestGenericCamera(unittest.TestCase):
'password': 'pass' 'password': 'pass'
}}) }})
image_view = self.hass.wsgi.mock_calls[0][1][0] yield from hass.loop.run_in_executor(None, setup_platform)
builder = EnvironBuilder(method='GET') client = yield from test_client(hass.http.app)
Request = request_class()
request = Request(builder.get_environ())
request.authenticated = True
resp = image_view.get(request, 'camera.config_test')
assert m.call_count == 1 resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello world'
image_view.get(request, 'camera.config_test') assert aioclient_mock.call_count == 1
assert m.call_count == 2 assert resp.status == 200
body = yield from resp.text()
assert body == 'hello world'
@requests_mock.Mocker() resp = yield from client.get('/api/camera_proxy/camera.config_test')
def test_limit_refetch(self, m): assert aioclient_mock.call_count == 2
"""Test that it fetches the given url."""
self.hass.wsgi = mock.MagicMock()
from requests.exceptions import Timeout
m.get('http://example.com/5a', text='hello world')
m.get('http://example.com/10a', text='hello world')
m.get('http://example.com/15a', text='hello planet')
m.get('http://example.com/20a', status_code=404)
assert setup_component(self.hass, 'camera', {
@asyncio.coroutine
def test_limit_refetch(aioclient_mock, hass, test_client):
"""Test that it fetches the given url."""
aioclient_mock.get('http://example.com/5a', text='hello world')
aioclient_mock.get('http://example.com/10a', text='hello world')
aioclient_mock.get('http://example.com/15a', text='hello planet')
aioclient_mock.get('http://example.com/20a', status=404)
def setup_platform():
"""Setup the platform."""
assert setup_component(hass, 'camera', {
'camera': { 'camera': {
'name': 'config_test', 'name': 'config_test',
'platform': 'generic', 'platform': 'generic',
@ -73,43 +55,47 @@ class TestGenericCamera(unittest.TestCase):
'limit_refetch_to_url_change': True, 'limit_refetch_to_url_change': True,
}}) }})
image_view = self.hass.wsgi.mock_calls[0][1][0] yield from hass.loop.run_in_executor(None, setup_platform)
builder = EnvironBuilder(method='GET') client = yield from test_client(hass.http.app)
Request = request_class()
request = Request(builder.get_environ())
request.authenticated = True
self.hass.states.set('sensor.temp', '5') resp = yield from client.get('/api/camera_proxy/camera.config_test')
with mock.patch('requests.get', side_effect=Timeout()): hass.states.async_set('sensor.temp', '5')
resp = image_view.get(request, 'camera.config_test')
assert m.call_count == 0
assert resp.status_code == 500, resp.response
self.hass.states.set('sensor.temp', '10') with mock.patch('async_timeout.timeout',
side_effect=asyncio.TimeoutError()):
resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 0
assert resp.status == 500
resp = image_view.get(request, 'camera.config_test') hass.states.async_set('sensor.temp', '10')
assert m.call_count == 1
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello world'
resp = image_view.get(request, 'camera.config_test') resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert m.call_count == 1 assert aioclient_mock.call_count == 1
assert resp.status_code == 200, resp.response assert resp.status == 200
assert resp.response[0].decode('utf-8') == 'hello world' body = yield from resp.text()
assert body == 'hello world'
self.hass.states.set('sensor.temp', '15') resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 1
assert resp.status == 200
body = yield from resp.text()
assert body == 'hello world'
# Url change = fetch new image hass.states.async_set('sensor.temp', '15')
resp = image_view.get(request, 'camera.config_test')
assert m.call_count == 2
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello planet'
# Cause a template render error # Url change = fetch new image
self.hass.states.remove('sensor.temp') resp = yield from client.get('/api/camera_proxy/camera.config_test')
resp = image_view.get(request, 'camera.config_test') assert aioclient_mock.call_count == 2
assert m.call_count == 2 assert resp.status == 200
assert resp.status_code == 200, resp.response body = yield from resp.text()
assert resp.response[0].decode('utf-8') == 'hello planet' assert body == 'hello planet'
# Cause a template render error
hass.states.async_remove('sensor.temp')
resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 2
assert resp.status == 200
body = yield from resp.text()
assert body == 'hello planet'

View file

@ -1,70 +1,60 @@
"""The tests for local file camera component.""" """The tests for local file camera component."""
import unittest import asyncio
from unittest import mock from unittest import mock
from werkzeug.test import EnvironBuilder # Using third party package because of a bug reading binary data in Python 3.4
# https://bugs.python.org/issue23004
from mock_open import MockOpen
from homeassistant.bootstrap import setup_component from homeassistant.bootstrap import setup_component
from homeassistant.components.http import request_class
from tests.common import get_test_home_assistant, assert_setup_component from tests.common import assert_setup_component, mock_http_component
class TestLocalCamera(unittest.TestCase): @asyncio.coroutine
"""Test the local file camera component.""" def test_loading_file(hass, test_client):
"""Test that it loads image from disk."""
@mock.patch('os.path.isfile', mock.Mock(return_value=True))
@mock.patch('os.access', mock.Mock(return_value=True))
def setup_platform():
"""Setup platform inside callback."""
assert setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'local_file',
'file_path': 'mock.file',
}})
def setUp(self): yield from hass.loop.run_in_executor(None, setup_platform)
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.wsgi = mock.MagicMock()
self.hass.config.components.append('http')
def tearDown(self): client = yield from test_client(hass.http.app)
"""Stop everything that was started."""
self.hass.stop()
def test_loading_file(self): m_open = MockOpen(read_data=b'hello')
"""Test that it loads image from disk.""" with mock.patch(
test_string = 'hello' 'homeassistant.components.camera.local_file.open',
self.hass.wsgi = mock.MagicMock() m_open, create=True
):
resp = yield from client.get('/api/camera_proxy/camera.config_test')
with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ assert resp.status == 200
mock.patch('os.access', mock.Mock(return_value=True)): body = yield from resp.text()
assert setup_component(self.hass, 'camera', { assert body == 'hello'
'camera': {
'name': 'config_test',
'platform': 'local_file',
'file_path': 'mock.file',
}})
image_view = self.hass.wsgi.mock_calls[0][1][0]
m_open = mock.mock_open(read_data=test_string) @asyncio.coroutine
with mock.patch( def test_file_not_readable(hass):
'homeassistant.components.camera.local_file.open', """Test local file will not setup when file is not readable."""
m_open, create=True mock_http_component(hass)
):
builder = EnvironBuilder(method='GET')
Request = request_class() # pylint: disable=invalid-name
request = Request(builder.get_environ())
request.authenticated = True
resp = image_view.get(request, 'camera.config_test')
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == test_string
def test_file_not_readable(self):
"""Test local file will not setup when file is not readable."""
self.hass.wsgi = mock.MagicMock()
def run_test():
with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \
mock.patch('os.access', return_value=False), \ mock.patch('os.access', return_value=False), \
assert_setup_component(0): assert_setup_component(0, 'camera'):
assert setup_component(self.hass, 'camera', { assert setup_component(hass, 'camera', {
'camera': { 'camera': {
'name': 'config_test', 'name': 'config_test',
'platform': 'local_file', 'platform': 'local_file',
'file_path': 'mock.file', 'file_path': 'mock.file',
}}) }})
assert [] == self.hass.states.all() yield from hass.loop.run_in_executor(None, run_test)

View file

@ -18,7 +18,7 @@ class TestUVCSetup(unittest.TestCase):
def setUp(self): def setUp(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.hass.wsgi = mock.MagicMock() self.hass.http = mock.MagicMock()
self.hass.config.components = ['http'] self.hass.config.components = ['http']
def tearDown(self): def tearDown(self):

View file

@ -18,42 +18,19 @@ HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT)
API_PASSWORD = "test1234" API_PASSWORD = "test1234"
HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD}
hass = None
entity_id = 'media_player.walkman' entity_id = 'media_player.walkman'
def setUpModule(): # pylint: disable=invalid-name
"""Initalize a Home Assistant server."""
global hass
hass = get_test_home_assistant()
setup_component(hass, http.DOMAIN, {
http.DOMAIN: {
http.CONF_SERVER_PORT: SERVER_PORT,
http.CONF_API_PASSWORD: API_PASSWORD,
},
})
hass.start()
time.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name
"""Stop the Home Assistant server."""
hass.stop()
class TestDemoMediaPlayer(unittest.TestCase): class TestDemoMediaPlayer(unittest.TestCase):
"""Test the media_player module.""" """Test the media_player module."""
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = hass self.hass = get_test_home_assistant()
try:
self.hass.config.components.remove(mp.DOMAIN) def tearDown(self):
except ValueError: """Shut down test instance."""
pass self.hass.stop()
def test_source_select(self): def test_source_select(self):
"""Test the input source service.""" """Test the input source service."""
@ -226,21 +203,6 @@ class TestDemoMediaPlayer(unittest.TestCase):
assert 0 == (mp.SUPPORT_PREVIOUS_TRACK & assert 0 == (mp.SUPPORT_PREVIOUS_TRACK &
state.attributes.get('supported_media_commands')) state.attributes.get('supported_media_commands'))
@requests_mock.Mocker(real_http=True)
def test_media_image_proxy(self, m):
"""Test the media server image proxy server ."""
fake_picture_data = 'test.test'
m.get('https://graph.facebook.com/v2.5/107771475912710/'
'picture?type=large', text=fake_picture_data)
assert setup_component(
self.hass, mp.DOMAIN,
{'media_player': {'platform': 'demo'}})
assert self.hass.states.is_state(entity_id, 'playing')
state = self.hass.states.get(entity_id)
req = requests.get(HTTP_BASE_URL +
state.attributes.get('entity_picture'))
assert req.text == fake_picture_data
@patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.' @patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.'
'media_seek') 'media_seek')
def test_play_media(self, mock_seek): def test_play_media(self, mock_seek):
@ -275,3 +237,42 @@ class TestDemoMediaPlayer(unittest.TestCase):
mp.media_seek(self.hass, 100, ent_id) mp.media_seek(self.hass, 100, ent_id)
self.hass.block_till_done() self.hass.block_till_done()
assert mock_seek.called assert mock_seek.called
class TestMediaPlayerWeb(unittest.TestCase):
"""Test the media player web views sensor."""
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
setup_component(self.hass, http.DOMAIN, {
http.DOMAIN: {
http.CONF_SERVER_PORT: SERVER_PORT,
http.CONF_API_PASSWORD: API_PASSWORD,
},
})
self.hass.start()
time.sleep(0.05)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@requests_mock.Mocker(real_http=True)
def test_media_image_proxy(self, m):
"""Test the media server image proxy server ."""
fake_picture_data = 'test.test'
m.get('https://graph.facebook.com/v2.5/107771475912710/'
'picture?type=large', text=fake_picture_data)
self.hass.block_till_done()
assert setup_component(
self.hass, mp.DOMAIN,
{'media_player': {'platform': 'demo'}})
assert self.hass.states.is_state(entity_id, 'playing')
state = self.hass.states.get(entity_id)
req = requests.get(HTTP_BASE_URL +
state.attributes.get('entity_picture'))
assert req.status_code == 200
assert req.text == fake_picture_data

View file

@ -1,10 +1,10 @@
"""Test HTML5 notify platform.""" """Test HTML5 notify platform."""
import asyncio
import json import json
from unittest.mock import patch, MagicMock, mock_open from unittest.mock import patch, MagicMock, mock_open
from werkzeug.test import EnvironBuilder from aiohttp import web
from homeassistant.components.http import request_class
from homeassistant.components.notify import html5 from homeassistant.components.notify import html5
SUBSCRIPTION_1 = { SUBSCRIPTION_1 = {
@ -35,6 +35,9 @@ SUBSCRIPTION_3 = {
}, },
} }
REGISTER_URL = '/api/notify.html5'
PUBLISH_URL = '/api/notify.html5/callback'
class TestHtml5Notify(object): class TestHtml5Notify(object):
"""Tests for HTML5 notify platform.""" """Tests for HTML5 notify platform."""
@ -94,9 +97,13 @@ class TestHtml5Notify(object):
assert payload['body'] == 'Hello' assert payload['body'] == 'Hello'
assert payload['icon'] == 'beer.png' assert payload['icon'] == 'beer.png'
def test_registering_new_device_view(self): @asyncio.coroutine
def test_registering_new_device_view(self, loop, test_client):
"""Test that the HTML view works.""" """Test that the HTML view works."""
hass = MagicMock() hass = MagicMock()
expected = {
'unnamed device': SUBSCRIPTION_1,
}
m = mock_open() m = mock_open()
with patch( with patch(
@ -114,21 +121,20 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value assert view.json_path == hass.config.path.return_value
assert view.registrations == {} assert view.registrations == {}
builder = EnvironBuilder(method='POST', app = web.Application(loop=loop)
data=json.dumps(SUBSCRIPTION_1)) view.register(app.router)
Request = request_class() client = yield from test_client(app)
resp = view.post(Request(builder.get_environ())) resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_1))
expected = { content = yield from resp.text()
'unnamed device': SUBSCRIPTION_1, assert resp.status == 200, content
}
assert resp.status_code == 200, resp.response
assert view.registrations == expected assert view.registrations == expected
handle = m() handle = m()
assert json.loads(handle.write.call_args[0][0]) == expected assert json.loads(handle.write.call_args[0][0]) == expected
def test_registering_new_device_validation(self): @asyncio.coroutine
def test_registering_new_device_validation(self, loop, test_client):
"""Test various errors when registering a new device.""" """Test various errors when registering a new device."""
hass = MagicMock() hass = MagicMock()
@ -146,34 +152,34 @@ class TestHtml5Notify(object):
view = hass.mock_calls[1][1][0] view = hass.mock_calls[1][1][0]
Request = request_class() app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
builder = EnvironBuilder(method='POST', data=json.dumps({ resp = yield from client.post(REGISTER_URL, data=json.dumps({
'browser': 'invalid browser', 'browser': 'invalid browser',
'subscription': 'sub info', 'subscription': 'sub info',
})) }))
resp = view.post(Request(builder.get_environ())) assert resp.status == 400
assert resp.status_code == 400, resp.response
builder = EnvironBuilder(method='POST', data=json.dumps({ resp = yield from client.post(REGISTER_URL, data=json.dumps({
'browser': 'chrome', 'browser': 'chrome',
})) }))
resp = view.post(Request(builder.get_environ())) assert resp.status == 400
assert resp.status_code == 400, resp.response
builder = EnvironBuilder(method='POST', data=json.dumps({
'browser': 'chrome',
'subscription': 'sub info',
}))
with patch('homeassistant.components.notify.html5._save_config', with patch('homeassistant.components.notify.html5._save_config',
return_value=False): return_value=False):
resp = view.post(Request(builder.get_environ())) # resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 400, resp.response resp = yield from client.post(REGISTER_URL, data=json.dumps({
'browser': 'chrome',
'subscription': 'sub info',
}))
@patch('homeassistant.components.notify.html5.os') assert resp.status == 400
def test_unregistering_device_view(self, mock_os):
@asyncio.coroutine
def test_unregistering_device_view(self, loop, test_client):
"""Test that the HTML unregister view works.""" """Test that the HTML unregister view works."""
mock_os.path.isfile.return_value = True
hass = MagicMock() hass = MagicMock()
config = { config = {
@ -182,11 +188,14 @@ class TestHtml5Notify(object):
} }
m = mock_open(read_data=json.dumps(config)) m = mock_open(read_data=json.dumps(config))
with patch(
'homeassistant.components.notify.html5.open', m, create=True with patch('homeassistant.components.notify.html5.open', m,
): create=True):
hass.config.path.return_value = 'file.conf' hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {})
with patch('homeassistant.components.notify.html5.os.path.isfile',
return_value=True):
service = html5.get_service(hass, {})
assert service is not None assert service is not None
@ -197,23 +206,25 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value assert view.json_path == hass.config.path.return_value
assert view.registrations == config assert view.registrations == config
builder = EnvironBuilder(method='DELETE', data=json.dumps({ app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
resp = yield from client.delete(REGISTER_URL, data=json.dumps({
'subscription': SUBSCRIPTION_1['subscription'], 'subscription': SUBSCRIPTION_1['subscription'],
})) }))
Request = request_class()
resp = view.delete(Request(builder.get_environ()))
config.pop('some device') config.pop('some device')
assert resp.status_code == 200, resp.response assert resp.status == 200, resp.response
assert view.registrations == config assert view.registrations == config
handle = m() handle = m()
assert json.loads(handle.write.call_args[0][0]) == config assert json.loads(handle.write.call_args[0][0]) == config
@patch('homeassistant.components.notify.html5.os') @asyncio.coroutine
def test_unregister_device_view_handle_unknown_subscription(self, mock_os): def test_unregister_device_view_handle_unknown_subscription(self, loop,
test_client):
"""Test that the HTML unregister view handles unknown subscriptions.""" """Test that the HTML unregister view handles unknown subscriptions."""
mock_os.path.isfile.return_value = True
hass = MagicMock() hass = MagicMock()
config = { config = {
@ -226,7 +237,9 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True 'homeassistant.components.notify.html5.open', m, create=True
): ):
hass.config.path.return_value = 'file.conf' hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {}) with patch('homeassistant.components.notify.html5.os.path.isfile',
return_value=True):
service = html5.get_service(hass, {})
assert service is not None assert service is not None
@ -237,21 +250,23 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value assert view.json_path == hass.config.path.return_value
assert view.registrations == config assert view.registrations == config
builder = EnvironBuilder(method='DELETE', data=json.dumps({ app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
resp = yield from client.delete(REGISTER_URL, data=json.dumps({
'subscription': SUBSCRIPTION_3['subscription'] 'subscription': SUBSCRIPTION_3['subscription']
})) }))
Request = request_class()
resp = view.delete(Request(builder.get_environ()))
assert resp.status_code == 200, resp.response assert resp.status == 200, resp.response
assert view.registrations == config assert view.registrations == config
handle = m() handle = m()
assert handle.write.call_count == 0 assert handle.write.call_count == 0
@patch('homeassistant.components.notify.html5.os') @asyncio.coroutine
def test_unregistering_device_view_handles_json_safe_error(self, mock_os): def test_unregistering_device_view_handles_json_safe_error(self, loop,
test_client):
"""Test that the HTML unregister view handles JSON write errors.""" """Test that the HTML unregister view handles JSON write errors."""
mock_os.path.isfile.return_value = True
hass = MagicMock() hass = MagicMock()
config = { config = {
@ -264,7 +279,9 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True 'homeassistant.components.notify.html5.open', m, create=True
): ):
hass.config.path.return_value = 'file.conf' hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {}) with patch('homeassistant.components.notify.html5.os.path.isfile',
return_value=True):
service = html5.get_service(hass, {})
assert service is not None assert service is not None
@ -275,21 +292,23 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value assert view.json_path == hass.config.path.return_value
assert view.registrations == config assert view.registrations == config
builder = EnvironBuilder(method='DELETE', data=json.dumps({ app = web.Application(loop=loop)
'subscription': SUBSCRIPTION_1['subscription'], view.register(app.router)
})) client = yield from test_client(app)
Request = request_class()
with patch('homeassistant.components.notify.html5._save_config', with patch('homeassistant.components.notify.html5._save_config',
return_value=False): return_value=False):
resp = view.delete(Request(builder.get_environ())) resp = yield from client.delete(REGISTER_URL, data=json.dumps({
'subscription': SUBSCRIPTION_1['subscription'],
}))
assert resp.status_code == 500, resp.response assert resp.status == 500, resp.response
assert view.registrations == config assert view.registrations == config
handle = m() handle = m()
assert handle.write.call_count == 0 assert handle.write.call_count == 0
def test_callback_view_no_jwt(self): @asyncio.coroutine
def test_callback_view_no_jwt(self, loop, test_client):
"""Test that the notification callback view works without JWT.""" """Test that the notification callback view works without JWT."""
hass = MagicMock() hass = MagicMock()
@ -307,20 +326,20 @@ class TestHtml5Notify(object):
view = hass.mock_calls[2][1][0] view = hass.mock_calls[2][1][0]
builder = EnvironBuilder(method='POST', data=json.dumps({ app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
resp = yield from client.post(PUBLISH_URL, data=json.dumps({
'type': 'push', 'type': 'push',
'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72' 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72'
})) }))
Request = request_class()
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 401, resp.response assert resp.status == 401, resp.response
@patch('homeassistant.components.notify.html5.os') @asyncio.coroutine
@patch('pywebpush.WebPusher') def test_callback_view_with_jwt(self, loop, test_client):
def test_callback_view_with_jwt(self, mock_wp, mock_os):
"""Test that the notification callback view works with JWT.""" """Test that the notification callback view works with JWT."""
mock_os.path.isfile.return_value = True
hass = MagicMock() hass = MagicMock()
data = { data = {
@ -332,15 +351,18 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True 'homeassistant.components.notify.html5.open', m, create=True
): ):
hass.config.path.return_value = 'file.conf' hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {'gcm_sender_id': '100'}) with patch('homeassistant.components.notify.html5.os.path.isfile',
return_value=True):
service = html5.get_service(hass, {'gcm_sender_id': '100'})
assert service is not None assert service is not None
# assert hass.called # assert hass.called
assert len(hass.mock_calls) == 3 assert len(hass.mock_calls) == 3
service.send_message('Hello', target=['device'], with patch('pywebpush.WebPusher') as mock_wp:
data={'icon': 'beer.png'}) service.send_message('Hello', target=['device'],
data={'icon': 'beer.png'})
assert len(mock_wp.mock_calls) == 2 assert len(mock_wp.mock_calls) == 2
@ -359,13 +381,14 @@ class TestHtml5Notify(object):
bearer_token = "Bearer {}".format(push_payload['data']['jwt']) bearer_token = "Bearer {}".format(push_payload['data']['jwt'])
builder = EnvironBuilder(method='POST', data=json.dumps({ app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
resp = yield from client.post(PUBLISH_URL, data=json.dumps({
'type': 'push', 'type': 'push',
}), headers={'Authorization': bearer_token}) }), headers={'Authorization': bearer_token})
Request = request_class()
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 200, resp.response assert resp.status == 200
returned = resp.response[0].decode('utf-8') body = yield from resp.json()
expected = '{"event": "push", "status": "ok"}' assert body == {"event": "push", "status": "ok"}
assert json.loads(returned) == json.loads(expected)

View file

@ -1,33 +1,29 @@
"""The tests for the Yr sensor platform.""" """The tests for the Yr sensor platform."""
from datetime import datetime from datetime import datetime
from unittest import TestCase
from unittest.mock import patch from unittest.mock import patch
import requests_mock
from homeassistant.bootstrap import _setup_component from homeassistant.bootstrap import _setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from tests.common import get_test_home_assistant, load_fixture from tests.common import get_test_home_assistant, load_fixture
class TestSensorYr(TestCase): class TestSensorYr:
"""Test the Yr sensor.""" """Test the Yr sensor."""
def setUp(self): def setup_method(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.hass.config.latitude = 32.87336 self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743 self.hass.config.longitude = 117.22743
def tearDown(self): def teardown_method(self):
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
@requests_mock.Mocker() def test_default_setup(self, requests_mock):
def test_default_setup(self, m):
"""Test the default setup.""" """Test the default setup."""
m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/',
text=load_fixture('yr.no.json')) text=load_fixture('yr.no.json'))
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.dt_util.utcnow', with patch('homeassistant.components.sensor.yr.dt_util.utcnow',
@ -42,11 +38,10 @@ class TestSensorYr(TestCase):
assert state.state.isnumeric() assert state.state.isnumeric()
assert state.attributes.get('unit_of_measurement') is None assert state.attributes.get('unit_of_measurement') is None
@requests_mock.Mocker() def test_custom_setup(self, requests_mock):
def test_custom_setup(self, m):
"""Test a custom setup.""" """Test a custom setup."""
m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', requests_mock.get('http://api.yr.no/weatherapi/locationforecast/1.9/',
text=load_fixture('yr.no.json')) text=load_fixture('yr.no.json'))
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.dt_util.utcnow', with patch('homeassistant.components.sensor.yr.dt_util.utcnow',

View file

@ -1,11 +1,13 @@
"""The tests for the Home Assistant API component.""" """The tests for the Home Assistant API component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import asyncio
from contextlib import closing from contextlib import closing
import json import json
import time import time
import unittest import unittest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from aiohttp import web
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -243,20 +245,18 @@ class TestAPI(unittest.TestCase):
def test_api_get_error_log(self): def test_api_get_error_log(self):
"""Test the return of the error log.""" """Test the return of the error log."""
test_string = 'Test String°'.encode('UTF-8') test_string = 'Test String°'
# Can't use read_data with wsgiserver in Python 3.4.2. Due to a @asyncio.coroutine
# bug in read_data, it can't handle byte types ('Type str doesn't def mock_send():
# support the buffer API'), but wsgiserver requires byte types """Mock file send."""
# ('WSGI Applications must yield bytes'). So just mock our own return web.Response(text=test_string)
# read method.
m_open = Mock(return_value=Mock( with patch('homeassistant.components.http.HomeAssistantView.file',
read=Mock(side_effect=[test_string])) Mock(return_value=mock_send())):
)
with patch('homeassistant.components.http.open', m_open, create=True):
req = requests.get(_url(const.URL_API_ERROR_LOG), req = requests.get(_url(const.URL_API_ERROR_LOG),
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(test_string, req.text.encode('UTF-8')) self.assertEqual(test_string, req.text)
self.assertIsNone(req.headers.get('expires')) self.assertIsNone(req.headers.get('expires'))
def test_api_get_event_listeners(self): def test_api_get_event_listeners(self):

View file

@ -34,12 +34,12 @@ def setUpModule(): # pylint: disable=invalid-name
hass.bus.listen('test_event', lambda _: _) hass.bus.listen('test_event', lambda _: _)
hass.states.set('test.test', 'a_state') hass.states.set('test.test', 'a_state')
bootstrap.setup_component( assert bootstrap.setup_component(
hass, http.DOMAIN, hass, http.DOMAIN,
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
http.CONF_SERVER_PORT: SERVER_PORT}}) http.CONF_SERVER_PORT: SERVER_PORT}})
bootstrap.setup_component(hass, 'frontend') assert bootstrap.setup_component(hass, 'frontend')
hass.start() hass.start()
time.sleep(0.05) time.sleep(0.05)
@ -71,7 +71,7 @@ class TestFrontend(unittest.TestCase):
self.assertIsNotNone(frontendjs) self.assertIsNotNone(frontendjs)
req = requests.head(_url(frontendjs.groups(0)[0])) req = requests.get(_url(frontendjs.groups(0)[0]))
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)

View file

@ -56,7 +56,7 @@ def setUpModule():
bootstrap.setup_component(hass, 'api') bootstrap.setup_component(hass, 'api')
hass.wsgi.trusted_networks = [ hass.http.trusted_networks = [
ip_network(trusted_network) ip_network(trusted_network)
for trusted_network in TRUSTED_NETWORKS] for trusted_network in TRUSTED_NETWORKS]
@ -159,12 +159,9 @@ class TestHttp:
headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL}) headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL})
allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS)
assert req.status_code == 200 assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL assert req.headers.get(allow_origin) == HTTP_BASE_URL
assert req.headers.get(allow_headers) == all_allow_headers
def test_cors_allowed_with_password_in_header(self): def test_cors_allowed_with_password_in_header(self):
"""Test cross origin resource sharing with password in header.""" """Test cross origin resource sharing with password in header."""
@ -175,12 +172,9 @@ class TestHttp:
req = requests.get(_url(const.URL_API), headers=headers) req = requests.get(_url(const.URL_API), headers=headers)
allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS)
assert req.status_code == 200 assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL assert req.headers.get(allow_origin) == HTTP_BASE_URL
assert req.headers.get(allow_headers) == all_allow_headers
def test_cors_denied_without_origin_header(self): def test_cors_denied_without_origin_header(self):
"""Test cross origin resource sharing with password in header.""" """Test cross origin resource sharing with password in header."""
@ -207,8 +201,8 @@ class TestHttp:
allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS)
assert req.status_code == 200 assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL assert req.headers.get(allow_origin) == HTTP_BASE_URL
assert req.headers.get(allow_headers) == all_allow_headers assert req.headers.get(allow_headers) == \
const.HTTP_HEADER_HA_AUTH.upper()

View file

@ -1,6 +1,7 @@
"""The tests for the InfluxDB component.""" """The tests for the InfluxDB component."""
import unittest import unittest
from unittest import mock from unittest import mock
from unittest.mock import patch
import influxdb as influx_client import influxdb as influx_client
@ -60,6 +61,8 @@ class TestInfluxDB(unittest.TestCase):
assert setup_component(self.hass, influxdb.DOMAIN, config) assert setup_component(self.hass, influxdb.DOMAIN, config)
@patch('homeassistant.components.persistent_notification.create',
mock.MagicMock())
def test_setup_missing_password(self, mock_client): def test_setup_missing_password(self, mock_client):
"""Test the setup with existing username and missing password.""" """Test the setup with existing username and missing password."""
config = { config = {

59
tests/conftest.py Normal file
View file

@ -0,0 +1,59 @@
"""Setup some common test helper things."""
import functools
import logging
import pytest
import requests_mock as _requests_mock
from homeassistant import util
from homeassistant.util import location
from .common import async_test_home_assistant
from .test_util.aiohttp import mock_aiohttp_client
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
def test_real(func):
"""Force a function to require a keyword _test_real to be passed in."""
@functools.wraps(func)
def guard_func(*args, **kwargs):
real = kwargs.pop('_test_real', None)
if not real:
raise Exception('Forgot to mock or pass "_test_real=True" to %s',
func.__name__)
return func(*args, **kwargs)
return guard_func
# Guard a few functions that would make network connections
location.detect_location_info = test_real(location.detect_location_info)
location.elevation = test_real(location.elevation)
util.get_local_ip = lambda: '127.0.0.1'
@pytest.fixture
def hass(loop):
"""Fixture to provide a test instance of HASS."""
hass = loop.run_until_complete(async_test_home_assistant(loop))
yield hass
loop.run_until_complete(hass.async_stop())
@pytest.fixture
def requests_mock():
"""Fixture to provide a requests mocker."""
with _requests_mock.mock() as m:
yield m
@pytest.fixture
def aioclient_mock():
"""Fixture to mock aioclient calls."""
with mock_aiohttp_client() as mock_session:
yield mock_session

View file

@ -1,4 +1,5 @@
"""Test state helpers.""" """Test state helpers."""
import asyncio
from datetime import timedelta from datetime import timedelta
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
@ -20,6 +21,42 @@ from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
from tests.common import get_test_home_assistant, mock_service from tests.common import get_test_home_assistant, mock_service
def test_async_track_states(event_loop):
"""Test AsyncTrackStates context manager."""
hass = get_test_home_assistant()
try:
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
point3 = point2 + timedelta(seconds=5)
@asyncio.coroutine
@patch('homeassistant.core.dt_util.utcnow')
def run_test(mock_utcnow):
"""Run the test."""
mock_utcnow.return_value = point2
with state.AsyncTrackStates(hass) as states:
mock_utcnow.return_value = point1
hass.states.set('light.test', 'on')
mock_utcnow.return_value = point2
hass.states.set('light.test2', 'on')
state2 = hass.states.get('light.test2')
mock_utcnow.return_value = point3
hass.states.set('light.test3', 'on')
state3 = hass.states.get('light.test3')
assert [state2, state3] == \
sorted(states, key=lambda state: state.entity_id)
event_loop.run_until_complete(run_test())
finally:
hass.stop()
class TestStateHelpers(unittest.TestCase): class TestStateHelpers(unittest.TestCase):
"""Test the Home Assistant event helpers.""" """Test the Home Assistant event helpers."""
@ -54,31 +91,6 @@ class TestStateHelpers(unittest.TestCase):
[state2, state3], [state2, state3],
state.get_changed_since([state1, state2, state3], point2)) state.get_changed_since([state1, state2, state3], point2))
def test_track_states(self):
"""Test tracking of states."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
point3 = point2 + timedelta(seconds=5)
with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow:
mock_utcnow.return_value = point2
with state.TrackStates(self.hass) as states:
mock_utcnow.return_value = point1
self.hass.states.set('light.test', 'on')
mock_utcnow.return_value = point2
self.hass.states.set('light.test2', 'on')
state2 = self.hass.states.get('light.test2')
mock_utcnow.return_value = point3
self.hass.states.set('light.test3', 'on')
state3 = self.hass.states.get('light.test3')
self.assertEqual(
sorted([state2, state3], key=lambda state: state.entity_id),
sorted(states, key=lambda state: state.entity_id))
def test_reproduce_with_no_entity(self): def test_reproduce_with_no_entity(self):
"""Test reproduce_state with no entity.""" """Test reproduce_state with no entity."""
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)

112
tests/test_util/aiohttp.py Normal file
View file

@ -0,0 +1,112 @@
"""Aiohttp test utils."""
import asyncio
from contextlib import contextmanager
import functools
import json as _json
from unittest import mock
class AiohttpClientMocker:
"""Mock Aiohttp client requests."""
def __init__(self):
"""Initialize the request mocker."""
self._mocks = []
self.mock_calls = []
def request(self, method, url, *,
status=200,
text=None,
content=None,
json=None):
"""Mock a request."""
if json:
text = _json.dumps(json)
if text:
content = text.encode('utf-8')
if content is None:
content = b''
self._mocks.append(AiohttpClientMockResponse(
method, url, status, content))
def get(self, *args, **kwargs):
"""Register a mock get request."""
self.request('get', *args, **kwargs)
def put(self, *args, **kwargs):
"""Register a mock put request."""
self.request('put', *args, **kwargs)
def post(self, *args, **kwargs):
"""Register a mock post request."""
self.request('post', *args, **kwargs)
def delete(self, *args, **kwargs):
"""Register a mock delete request."""
self.request('delete', *args, **kwargs)
def options(self, *args, **kwargs):
"""Register a mock options request."""
self.request('options', *args, **kwargs)
@property
def call_count(self):
"""Number of requests made."""
return len(self.mock_calls)
@asyncio.coroutine
def match_request(self, method, url):
"""Match a request against pre-registered requests."""
for response in self._mocks:
if response.match_request(method, url):
self.mock_calls.append((method, url))
return response
assert False, "No mock registered for {} {}".format(method.upper(),
url)
class AiohttpClientMockResponse:
"""Mock Aiohttp client response."""
def __init__(self, method, url, status, response):
"""Initialize a fake response."""
self.method = method
self.url = url
self.status = status
self.response = response
def match_request(self, method, url):
"""Test if response answers request."""
return method == self.method and url == self.url
@asyncio.coroutine
def read(self):
"""Return mock response."""
return self.response
@asyncio.coroutine
def text(self, encoding='utf-8'):
"""Return mock response as a string."""
return self.response.decode(encoding)
@asyncio.coroutine
def release(self):
"""Mock release."""
pass
@contextmanager
def mock_aiohttp_client():
"""Context manager to mock aiohttp client."""
mocker = AiohttpClientMocker()
with mock.patch('aiohttp.ClientSession') as mock_session:
instance = mock_session()
for method in ('get', 'post', 'put', 'options', 'delete'):
setattr(instance, method,
functools.partial(mocker.match_request, method))
yield mocker