1
0
mirror of https://github.com/home-assistant/core synced 2024-07-08 20:17:01 +00:00

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:
fast_finish: true
include:
- python: "3.4"
- python: "3.4.2"
env: TOXENV=py34
- python: "3.4"
- python: "3.4.2"
env: TOXENV=requirements
- python: "3.5"
- python: "3.4.2"
env: TOXENV=lint
- python: "3.5"
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
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
try:
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
https://home-assistant.io/components/alexa/
"""
import asyncio
import copy
import enum
import logging
@ -12,6 +13,7 @@ from datetime import datetime
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView
@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
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_CARD = 'card'
@ -102,8 +104,8 @@ def setup(hass, config):
intents = config[DOMAIN].get(CONF_INTENTS, {})
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
hass.wsgi.register_view(AlexaIntentsView(hass, intents))
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings))
hass.http.register_view(AlexaIntentsView(hass, intents))
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
return True
@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView):
self.intents = intents
@asyncio.coroutine
def post(self, request):
"""Handle Alexa."""
data = request.json
data = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data)
@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView):
action = config.get(CONF_ACTION)
if action is not None:
action.run(response.variables)
yield from action.async_run(response.variables)
# pylint: disable=unsubscriptable-object
if speech is not None:
@ -218,8 +221,8 @@ class AlexaResponse(object):
self.card = card
return
card["title"] = title.render(self.variables)
card["content"] = content.render(self.variables)
card["title"] = title.async_render(self.variables)
card["content"] = content.async_render(self.variables)
self.card = card
def add_speech(self, speech_type, text):
@ -229,7 +232,7 @@ class AlexaResponse(object):
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
if isinstance(text, template.Template):
text = text.render(self.variables)
text = text.async_render(self.variables)
self.speech = {
'type': speech_type.value,
@ -244,7 +247,7 @@ class AlexaResponse(object):
self.reprompt = {
'type': speech_type.value,
key: text.render(self.variables)
key: text.async_render(self.variables)
}
def as_dict(self):
@ -284,6 +287,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
template.attach(hass, self.flash_briefings)
# pylint: disable=too-many-branches
@callback
def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request."""
_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:
err = 'No configured Alexa flash briefing was found for: %s'
_LOGGER.error(err, briefing_id)
return self.Response(status=404)
return b'', 404
briefing = []
@ -300,13 +304,13 @@ class AlexaFlashBriefingView(HomeAssistantView):
output = {}
if item.get(CONF_TITLE) is not None:
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:
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
if item.get(CONF_TEXT) is not None:
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:
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
@ -315,7 +319,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if item.get(CONF_AUDIO) is not None:
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:
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
@ -323,7 +327,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if isinstance(item.get(CONF_DISPLAY_URL),
template.Template):
output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].render()
item[CONF_DISPLAY_URL].async_render()
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)

View File

@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
import asyncio
import json
import logging
import queue
from aiohttp import web
import async_timeout
import homeassistant.core as ha
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,
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView
@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Register the API with the HTTP interface."""
hass.wsgi.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView)
hass.wsgi.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView)
hass.wsgi.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView)
hass.wsgi.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView)
hass.wsgi.register_view(APITemplateView)
hass.http.register_view(APIStatusView)
hass.http.register_view(APIEventStream)
hass.http.register_view(APIConfigView)
hass.http.register_view(APIDiscoveryView)
hass.http.register_view(APIStatesView)
hass.http.register_view(APIEntityStateView)
hass.http.register_view(APIEventListenersView)
hass.http.register_view(APIEventView)
hass.http.register_view(APIServicesView)
hass.http.register_view(APIDomainServicesView)
hass.http.register_view(APIEventForwardingView)
hass.http.register_view(APIComponentsView)
hass.http.register_view(APIErrorLogView)
hass.http.register_view(APITemplateView)
return True
@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
url = URL_API
name = "api:status"
@ha.callback
def get(self, request):
"""Retrieve if API is running."""
return self.json_message('API running.')
@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM
name = "api:stream"
@asyncio.coroutine
def get(self, request):
"""Provide a streaming interface for the event bus."""
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:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView):
else:
data = json.dumps(event, cls=rem.JSONEncoder)
to_write.put(data)
yield from to_write.put(data)
def stream():
"""Stream events to response."""
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events)
response = web.StreamResponse()
response.content_type = 'text/event-stream'
yield from response.prepare(request)
try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
# Fire off one message so browsers fire open event right away
to_write.put(STREAM_PING_PAYLOAD)
try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
while True:
try:
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
# Fire off one message so browsers fire open event right away
yield from to_write.put(STREAM_PING_PAYLOAD)
if payload is stop_obj:
break
while True:
try:
with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=self.hass.loop):
payload = yield from to_write.get()
msg = "data: {}\n\n".format(payload)
_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:
if payload is stop_obj:
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):
@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
url = URL_API_CONFIG
name = "api:config"
@ha.callback
def get(self, request):
"""Get current configuration."""
return self.json(self.hass.config.as_dict())
@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
url = URL_API_DISCOVERY_INFO
name = "api:discovery"
@ha.callback
def get(self, request):
"""Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None
@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
url = URL_API_STATES
name = "api:states"
@ha.callback
def get(self, request):
"""Get current states."""
return self.json(self.hass.states.all())
return self.json(self.hass.states.async_all())
class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests."""
url = "/api/states/<entity(exist=False):entity_id>"
url = "/api/states/{entity_id}"
name = "api:entity-state"
@ha.callback
def get(self, request, entity_id):
"""Retrieve state of entity."""
state = self.hass.states.get(entity_id)
@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine
def post(self, request, entity_id):
"""Update state of entity."""
try:
new_state = request.json['state']
except KeyError:
data = yield from request.json()
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)
attributes = request.json.get('attributes')
force_update = request.json.get('force_update', False)
attributes = data.get('attributes')
force_update = data.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None
# 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
resp = self.json(self.hass.states.get(entity_id))
if is_new_state:
resp.status_code = HTTP_CREATED
status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(self.hass.states.get(entity_id), status_code)
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
return resp
@ha.callback
def delete(self, request, entity_id):
"""Remove entity."""
if self.hass.states.remove(entity_id):
if self.hass.states.async_remove(entity_id):
return self.json_message('Entity removed')
else:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
url = URL_API_EVENTS
name = "api:event-listeners"
@ha.callback
def get(self, request):
"""Get event listeners."""
return self.json(events_json(self.hass))
return self.json(async_events_json(self.hass))
class APIEventView(HomeAssistantView):
"""View to handle Event requests."""
url = '/api/events/<event_type>'
url = '/api/events/{event_type}'
name = "api:event"
@asyncio.coroutine
def post(self, request, event_type):
"""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):
return self.json_message('Event data should be a JSON object',
@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
if 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))
@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES
name = "api:services"
@ha.callback
def get(self, request):
"""Get registered services."""
return self.json(services_json(self.hass))
return self.json(async_services_json(self.hass))
class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>"
url = "/api/services/{domain}/{service}"
name = "api:domain-services"
@asyncio.coroutine
def post(self, request, domain, service):
"""Call a service.
Returns a list of changed states.
"""
with TrackStates(self.hass) as changed_states:
self.hass.services.call(domain, service, request.json, True)
body = yield from request.text()
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)
@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
name = "api:event-forward"
event_forwarder = None
@asyncio.coroutine
def post(self, request):
"""Setup an event forwarder."""
data = request.json
if data is None:
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
host = data['host']
api_password = data['api_password']
@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
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.",
HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None:
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.")
@asyncio.coroutine
def delete(self, request):
"""Remove event forwarer."""
data = request.json
if data is None:
"""Remove event forwarder."""
try:
data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
if self.event_forwarder is not None:
api = rem.API(host, None, port)
self.event_forwarder.disconnect(api)
self.event_forwarder.async_disconnect(api)
return self.json_message("Event forwarding cancelled.")
@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
url = URL_API_COMPONENTS
name = "api:components"
@ha.callback
def get(self, request):
"""Get current loaded components."""
return self.json(self.hass.config.components)
@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
url = URL_API_ERROR_LOG
name = "api:error-log"
@asyncio.coroutine
def get(self, request):
"""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):
@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE
name = "api:template"
@asyncio.coroutine
def post(self, request):
"""Render a template."""
try:
tpl = template.Template(request.json['template'], self.hass)
return tpl.render(request.json.get('variables'))
except TemplateError as ex:
data = yield from request.json()
tpl = template.Template(data['template'], self.hass)
return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST)
def services_json(hass):
def async_services_json(hass):
"""Generate services data to JSONify."""
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."""
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
# check source
if not run_test(config.get(CONF_INPUT)):
if not run_test(hass, config.get(CONF_INPUT)):
return
# 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
https://home-assistant.io/components/camera/
"""
import asyncio
import logging
import time
from aiohttp import web
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@ -31,8 +33,8 @@ def setup(hass, config):
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
hass.http.register_view(CameraImageView(hass, component.entities))
hass.http.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config)
@ -80,33 +82,59 @@ class Camera(Entity):
"""Return bytes of camera image."""
raise NotImplementedError()
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images."""
def stream():
"""Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
@asyncio.coroutine
def async_camera_image(self):
"""Return bytes of camera image.
if img_bytes is not None and img_bytes != last_image:
yield 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'
This method must be run in the event loop.
"""
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
return image
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)
except GeneratorExit:
pass
This method must be run in the event loop.
"""
response = web.StreamResponse()
return response(
stream(),
content_type=('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
)
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
response.enable_chunked_encoding()
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
def state(self):
@ -144,22 +172,25 @@ class CameraView(HomeAssistantView):
super().__init__(hass)
self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id):
"""Start a get request."""
camera = self.entities.get(entity_id)
if camera is None:
return self.Response(status=404)
return web.Response(status=404)
authenticated = (request.authenticated or
request.args.get('token') == camera.access_token)
request.GET.get('token') == camera.access_token)
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."""
raise NotImplementedError()
@ -167,25 +198,27 @@ class CameraView(HomeAssistantView):
class CameraImageView(CameraView):
"""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"
def handle(self, camera):
@asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image."""
response = camera.camera_image()
image = yield from camera.async_camera_image()
if response is None:
return self.Response(status=500)
if image is None:
return web.Response(status=500)
return self.Response(response)
return web.Response(body=image)
class CameraMjpegStream(CameraView):
"""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"
def handle(self, camera):
@asyncio.coroutine
def handle(self, request, camera):
"""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
https://home-assistant.io/components/camera.ffmpeg/
"""
import asyncio
import logging
import voluptuous as vol
from aiohttp import web
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
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
from homeassistant.const import CONF_NAME
from homeassistant.util.async import run_coroutine_threadsafe
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."""
if not run_test(config.get(CONF_INPUT)):
if not async_run_test(hass, config.get(CONF_INPUT)):
return
add_devices([FFmpegCamera(config)])
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera."""
def __init__(self, config):
def __init__(self, hass, config):
"""Initialize a FFmpeg camera."""
super().__init__()
self._name = config.get(CONF_NAME)
@ -45,24 +49,45 @@ class FFmpegCamera(Camera):
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
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."""
from haffmpeg import ImageSingle, IMAGE_JPEG
ffmpeg = ImageSingle(get_binary())
from haffmpeg import ImageSingleAsync, IMAGE_JPEG
ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments)
image = yield from ffmpeg.get_image(
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."""
from haffmpeg import CameraMjpeg
from haffmpeg import CameraMjpegAsync
stream = CameraMjpeg(get_binary())
stream.open_camera(self._input, extra_cmd=self._extra_arguments)
return response(
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver',
direct_passthrough=True
)
stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
yield from stream.open_camera(
self._input, extra_cmd=self._extra_arguments)
response = web.StreamResponse()
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
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
https://home-assistant.io/components/camera.generic/
"""
import asyncio
import logging
import aiohttp
import async_timeout
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from requests.auth import HTTPDigestAuth
import voluptuous as vol
from homeassistant.const import (
@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
@ -35,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
# 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."""
add_devices([GenericCamera(hass, config)])
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
# pylint: disable=too-many-instance-attributes
@ -49,6 +54,7 @@ class GenericCamera(Camera):
"""Initialize a generic camera."""
super().__init__()
self.hass = hass
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass
@ -58,20 +64,27 @@ class GenericCamera(Camera):
password = device_info.get(CONF_PASSWORD)
if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password)
else:
self._auth = HTTPBasicAuth(username, password)
self._auth = aiohttp.BasicAuth(username, password=password)
else:
self._auth = None
self._last_url = None
self._last_image = None
self._session = aiohttp.ClientSession(loop=hass.loop, auth=self._auth)
def camera_image(self):
"""Return bytes of camera image."""
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."""
try:
url = self._still_image_url.render()
url = self._still_image_url.async_render()
except TemplateError as err:
_LOGGER.error('Error parsing template %s: %s',
self._still_image_url, err)
@ -80,16 +93,32 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch:
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:
response = requests.get(url, **kwargs)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
self._last_image = yield from self.hass.loop.run_in_executor(
None, fetch)
# async
else:
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_image = response.content
return self._last_image
@property

View File

@ -4,9 +4,14 @@ Support for IP Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.mjpeg/
"""
import asyncio
import logging
from contextlib import closing
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
# 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."""
add_devices([MjpegCamera(config)])
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
def extract_image_from_mjpeg(stream):
@ -56,7 +62,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera):
"""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."""
super().__init__()
self._name = device_info.get(CONF_NAME)
@ -65,32 +71,57 @@ class MjpegCamera(Camera):
self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL]
def camera_stream(self):
"""Return a MJPEG stream image response directly from the camera."""
auth = None
if self._authentication == HTTP_BASIC_AUTHENTICATION:
auth = aiohttp.BasicAuth(self._username, password=self._password)
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth)
def camera_image(self):
"""Return a still image response from the camera."""
if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password)
else:
auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url,
auth=auth,
stream=True, timeout=10)
req = requests.get(
self._mjpeg_url, auth=auth, stream=True, timeout=10)
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):
"""Return a still image response from the camera."""
with closing(self.camera_stream()) as response:
return extract_image_from_mjpeg(response.iter_content(1024))
with closing(req) as response:
return extract_image_from_mjpeg(response.iter_content(102400))
def mjpeg_stream(self, response):
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
stream = self.camera_stream()
return response(
stream.iter_content(chunk_size=1024),
mimetype=stream.headers[CONTENT_TYPE_HEADER],
direct_passthrough=True
)
# aiohttp don't support DigestAuth -> Fallback
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
yield from super().handle_async_mjpeg_stream(request)
return
# 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
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
https://home-assistant.io/components/device_tracker.locative/
"""
import asyncio
from functools import partial
import logging
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
@ -19,7 +21,7 @@ DEPENDENCIES = ['http']
def setup_scanner(hass, config, see):
"""Setup an endpoint for the Locative application."""
hass.wsgi.register_view(LocativeView(hass, see))
hass.http.register_view(LocativeView(hass, see))
return True
@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView):
super().__init__(hass)
self.see = see
@asyncio.coroutine
def get(self, request):
"""Locative message received as GET."""
return self.post(request)
res = yield from self._handle(request.GET)
return res
@asyncio.coroutine
def post(self, request):
"""Locative message received."""
# pylint: disable=too-many-return-statements
data = request.values
data = yield from request.post()
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:
return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY)
@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView):
direction = data['trigger']
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)
elif direction == 'exit':
@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView):
'{}.{}'.format(DOMAIN, device))
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'
else:
# 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
https://home-assistant.io/components/emulated_hue/
"""
import asyncio
import threading
import socket
import logging
import json
import os
import select
from aiohttp import web
import voluptuous as vol
from homeassistant import util, core
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON, HTTP_BAD_REQUEST
STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@ -25,8 +26,6 @@ from homeassistant.components.light import (
from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI
)
# pylint: disable=unused-import
from homeassistant.components.http import REQUIREMENTS # noqa
import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue'
@ -87,19 +86,21 @@ def setup(hass, yaml_config):
upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port)
def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
server.start()
upnp_listener.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
@core.callback
def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
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
@ -158,6 +159,7 @@ class DescriptionXmlView(HomeAssistantView):
super().__init__(hass)
self.config = config
@core.callback
def get(self, request):
"""Handle a GET request."""
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
@ -185,7 +187,7 @@ class DescriptionXmlView(HomeAssistantView):
resp_text = xml_template.format(
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):
@ -200,9 +202,13 @@ class HueUsernameView(HomeAssistantView):
"""Initialize the instance of the view."""
super().__init__(hass)
@asyncio.coroutine
def post(self, 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:
return self.json_message('devicetype not specified',
@ -214,10 +220,10 @@ class HueUsernameView(HomeAssistantView):
class HueLightsView(HomeAssistantView):
"""Handle requests for getting and setting info about entities."""
url = '/api/<username>/lights'
url = '/api/{username}/lights'
name = 'api:username:lights'
extra_urls = ['/api/<username>/lights/<entity_id>',
'/api/<username>/lights/<entity_id>/state']
extra_urls = ['/api/{username}/lights/{entity_id}',
'/api/{username}/lights/{entity_id}/state']
requires_auth = False
def __init__(self, hass, config):
@ -226,58 +232,51 @@ class HueLightsView(HomeAssistantView):
self.config = config
self.cached_states = {}
@core.callback
def get(self, request, username, entity_id=None):
"""Handle a GET request."""
if entity_id is None:
return self.get_lights_list()
return self.async_get_lights_list()
if not request.base_url.endswith('state'):
return self.get_light_state(entity_id)
if not request.path.endswith('state'):
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):
"""Handle a PUT request."""
if not request.base_url.endswith('state'):
return self.Response("Method not allowed", status=405)
if not request.path.endswith('state'):
return web.Response(text="Method not allowed", status=405)
content_type = request.environ.get('CONTENT_TYPE', '')
if content_type == 'application/x-www-form-urlencoded':
# 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
if entity_id and self.hass.states.get(entity_id) is None:
return self.json_message('Entity not found', HTTP_NOT_FOUND)
for key in request.form:
try:
json_data = json.loads(key)
break
except ValueError:
# Try the next key?
pass
try:
json_data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
if json_data is None:
return self.Response("Bad request", status=400)
else:
json_data = request.json
result = yield from self.async_put_light_state(json_data, entity_id)
return result
return self.put_light_state(json_data, entity_id)
def get_lights_list(self):
@core.callback
def async_get_lights_list(self):
"""Process a request to get the list of available lights."""
json_response = {}
for entity in self.hass.states.all():
for entity in self.hass.states.async_all():
if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity)
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."""
entity = self.hass.states.get(entity_id)
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)
@ -292,23 +291,24 @@ class HueLightsView(HomeAssistantView):
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."""
config = self.config
# Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id)
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):
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
parsed = parse_hue_api_put_light_body(request_json, entity)
if parsed is None:
return self.Response("Bad request", status=400)
return web.Response(text="Bad request", status=400)
result, brightness = parsed
@ -333,7 +333,8 @@ class HueLightsView(HomeAssistantView):
self.cached_states[entity_id] = (result, brightness)
# 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 = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
@ -345,7 +346,10 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response)
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
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
https://home-assistant.io/components/ffmpeg/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'ffmpeg'
REQUIREMENTS = ["ha-ffmpeg==0.13"]
REQUIREMENTS = ["ha-ffmpeg==0.14"]
_LOGGER = logging.getLogger(__name__)
@ -47,13 +49,26 @@ def setup(hass, config):
def get_binary():
"""Return ffmpeg binary from config."""
"""Return ffmpeg binary from config.
Async friendly.
"""
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."""
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 in cache
@ -61,8 +76,9 @@ def run_test(input_source):
return FFMPEG_TEST_CACHE[input_source]
# run test
test = Test(get_binary())
if not test.run_test(input_source):
ffmpeg_test = TestAsync(get_binary(), loop=hass.loop)
success = yield from ffmpeg_test.run_test(input_source)
if not success:
_LOGGER.error("FFmpeg '%s' test fails!", input_source)
FFMPEG_TEST_CACHE[input_source] = 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
https://home-assistant.io/components/foursquare/
"""
import asyncio
import logging
import os
import json
import requests
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
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
@ -75,7 +75,7 @@ def setup(hass, config):
descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA)
hass.wsgi.register_view(FoursquarePushReceiver(
hass.http.register_view(FoursquarePushReceiver(
hass, config[CONF_PUSH_SECRET]))
return True
@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView):
super().__init__(hass)
self.push_secret = push_secret
@asyncio.coroutine
def post(self, request):
"""Accept the POST from Foursquare."""
raw_data = request.form
_LOGGER.debug("Received Foursquare push: %s", raw_data)
if self.push_secret != raw_data["secret"]:
try:
data = yield from request.json()
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"
"push secret! Data: %s", raw_data)
return
parsed_payload = {
key: json.loads(val) for key, val in raw_data.items()
if key != "secret"
}
self.hass.bus.fire(EVENT_PUSH, parsed_payload)
"push secret: %s", secret)
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
self.hass.bus.async_fire(EVENT_PUSH, data)

View File

@ -1,8 +1,13 @@
"""Handle the frontend for Home Assistant."""
import asyncio
import hashlib
import json
import logging
import os
from aiohttp import web
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api
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
path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development:
if hass.http.development:
url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
else:
@ -98,7 +103,7 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path)
hass.http.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url)
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):
"""Setup serving the frontend."""
hass.wsgi.register_view(BootstrapView)
hass.wsgi.register_view(ManifestJSONView)
hass.http.register_view(BootstrapView)
hass.http.register_view(ManifestJSONView)
if hass.wsgi.development:
if hass.http.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
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)
hass.wsgi.register_static_path("/robots.txt",
hass.http.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
hass.http.register_static_path("/static", STATIC_PATH)
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')
@ -140,7 +148,7 @@ def setup(hass, config):
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.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
@ -161,13 +169,14 @@ class BootstrapView(HomeAssistantView):
url = "/api/bootstrap"
name = "api:bootstrap"
@callback
def get(self, request):
"""Return all data needed to bootstrap Home Assistant."""
return self.json({
'config': self.hass.config.as_dict(),
'states': self.hass.states.all(),
'events': api.events_json(self.hass),
'services': api.services_json(self.hass),
'states': self.hass.states.async_all(),
'events': api.async_events_json(self.hass),
'services': api.async_services_json(self.hass),
'panels': PANELS,
})
@ -193,9 +202,10 @@ class IndexView(HomeAssistantView):
)
)
@asyncio.coroutine
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
if self.hass.http.development:
core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else:
@ -215,22 +225,24 @@ class IndexView(HomeAssistantView):
if self.hass.config.api.api_password:
# require password if set
no_auth = 'false'
if self.hass.wsgi.is_trusted_ip(
self.hass.wsgi.get_real_ip(request)):
if self.hass.http.is_trusted_ip(
self.hass.http.get_real_ip(request)):
# bypass for trusted networks
no_auth = 'true'
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: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
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):
@ -240,8 +252,8 @@ class ManifestJSONView(HomeAssistantView):
url = "/manifest.json"
name = "manifestjson"
def get(self, request):
@asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
import json
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
https://home-assistant.io/components/history/
"""
import asyncio
from collections import defaultdict
from datetime import timedelta
from itertools import groupby
import voluptuous as vol
from homeassistant.const import HTTP_BAD_REQUEST
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
@ -182,8 +184,8 @@ def setup(hass, config):
filters.included_entities = include[CONF_ENTITIES]
filters.included_domains = include[CONF_DOMAINS]
hass.wsgi.register_view(Last5StatesView(hass))
hass.wsgi.register_view(HistoryPeriodView(hass, filters))
hass.http.register_view(Last5StatesView(hass))
hass.http.register_view(HistoryPeriodView(hass, filters))
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True
@ -192,16 +194,19 @@ def setup(hass, config):
class Last5StatesView(HomeAssistantView):
"""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'
def __init__(self, hass):
"""Initilalize the history last 5 states view."""
super().__init__(hass)
@asyncio.coroutine
def get(self, request, entity_id):
"""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):
@ -209,15 +214,22 @@ class HistoryPeriodView(HomeAssistantView):
url = '/api/history/period'
name = 'api:history:view-period'
extra_urls = ['/api/history/period/<datetime:datetime>']
extra_urls = ['/api/history/period/{datetime}']
def __init__(self, hass, filters):
"""Initilalize the history period view."""
super().__init__(hass)
self.filters = filters
@asyncio.coroutine
def get(self, request, datetime=None):
"""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)
if datetime:
@ -226,10 +238,13 @@ class HistoryPeriodView(HomeAssistantView):
start_time = dt_util.utcnow() - 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(
start_time, end_time, entity_id, self.filters).values())
result = yield from self.hass.loop.run_in_executor(
None, get_significant_states, start_time, end_time, entity_id,
self.filters)
return self.json(result.values())
# 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
https://home-assistant.io/components/http/
"""
import asyncio
import hmac
import json
import logging
import mimetypes
import threading
import os
from pathlib import Path
import re
import ssl
from ipaddress import ip_address, ip_network
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
from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE_JSON,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import split_entity_id
import homeassistant.util.dt as dt_util
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification
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_SERVER_HOST = 'server_host'
@ -83,6 +88,12 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
# TEMP TO GET TESTS TO RUN
def request_class():
"""."""
raise Exception('not implemented')
class HideSensitiveFilter(logging.Filter):
"""Filter API password calls."""
@ -94,17 +105,17 @@ class HideSensitiveFilter(logging.Filter):
def filter(self, record):
"""Hide sensitive data in messages."""
if self.hass.wsgi.api_password is None:
if self.hass.http.api_password is None:
return True
record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******')
record.msg = record.msg.replace(self.hass.http.api_password, '*******')
return True
def setup(hass, config):
"""Set up the HTTP API and debug interface."""
_LOGGER.addFilter(HideSensitiveFilter(hass))
logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass))
conf = config.get(DOMAIN, {})
@ -131,19 +142,20 @@ def setup(hass, config):
trusted_networks=trusted_networks
)
def start_wsgi_server(event):
"""Start the WSGI server."""
server.start()
@callback
def stop_server(event):
"""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):
"""Stop the WSGI server."""
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)
hass.wsgi = server
hass.http = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
else util.get_local_ip(),
api_password, server_port,
@ -152,105 +164,84 @@ def setup(hass, config):
return True
def request_class():
"""Generate request class.
class GzipFileSender(FileSender):
"""FileSender class capable of sending gzip version if available."""
Done in method because of imports.
"""
from werkzeug.exceptions import BadRequest
from werkzeug.wrappers import BaseRequest, AcceptMixin
from werkzeug.utils import cached_property
# pylint: disable=invalid-name, too-few-public-methods
class Request(BaseRequest, AcceptMixin):
"""Base class for incoming requests."""
development = False
@cached_property
def json(self):
"""Get the result of json.loads if possible."""
if not self.data:
return None
# elif 'json' not in self.environ.get('CONTENT_TYPE', ''):
# 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')
@asyncio.coroutine
def send(self, request, filepath):
"""Send filepath to client using request."""
gzip = False
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
gzip_path = filepath.with_name(filepath.name + '.gz')
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):
"""Generate empty routing map with HA validators."""
from werkzeug.routing import Map, BaseConverter, ValidationError
class HAStaticRoute(StaticRoute):
"""StaticRoute with support for fingerprinting."""
class EntityValidator(BaseConverter):
"""Validate entity_id in urls."""
def __init__(self, prefix, path):
"""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):
"""Initilalize entity validator."""
super().__init__(url_map)
self._exist = exist
self._domain = domain
# Extra sauce to remove fingerprinted resource names
filename = path[self._prefix_len:]
fingerprinted = _FINGERPRINT.match(filename)
if fingerprinted:
filename = '{}.{}'.format(*fingerprinted.groups())
def to_python(self, value):
"""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,
})
return {'filename': filename}
class HomeAssistantWSGI(object):
@ -262,28 +253,35 @@ class HomeAssistantWSGI(object):
def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
trusted_networks):
"""Initilalize the WSGI Home Assistant server."""
from werkzeug.wrappers import Response
"""Initialize the WSGI Home Assistant server."""
import aiohttp_cors
Response.mimetype = 'text/html'
# pylint: disable=invalid-name
self.Request = request_class()
self.url_map = routing_map(hass)
self.views = {}
self.app = web.Application(loop=hass.loop)
self.hass = hass
self.extra_apps = {}
self.development = development
self.api_password = api_password
self.ssl_certificate = ssl_certificate
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
self.cors_origins = cors_origins
self.trusted_networks = trusted_networks
self.event_forwarder = None
self._handler = 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):
"""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
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):
# Instantiate the view, if needed
view = view(self.hass)
self.views[view.name] = view
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)
view.register(self.app.router)
def register_redirect(self, url, redirect_to):
"""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
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):
"""Register a folder to serve as a static path.
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:
# 1 year in seconds
cache_time = cache_length * 86400
@asyncio.coroutine
def serve_file(request):
"""Redirect to location."""
return _GZIP_FILE_SENDER.send(request, filepath)
headers.append({
'prefix': '',
HTTP_HEADER_CACHE_CONTROL:
"public, max-age={}".format(cache_time)
})
# aiohttp supports regex matching for variables. Using that as temp
# to work around cache busting MD5.
# Turns something like /static/dev-panel.html into
# /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))
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
self.app.router.add_route('GET', url_pattern, serve_file)
@asyncio.coroutine
def start(self):
"""Start the wsgi server."""
from cherrypy import wsgiserver
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
# 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.cors is not None:
for route in list(self.app.router.routes()):
self.cors.add(route)
if self.ssl_certificate:
context = ssl.SSLContext(SSL_VERSION)
context.options |= SSL_OPTS
context.set_ciphers(CIPHERS)
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
self.server.ssl_adapter = ContextSSLAdapter(context)
else:
context = None
threading.Thread(
target=self.server.start, daemon=True, name='WSGI-server').start()
self._handler = self.app.make_handler()
self.server = yield from self.hass.loop.create_server(
self._handler, self.server_host, self.server_port, ssl=context)
@asyncio.coroutine
def stop(self):
"""Stop the wsgi server."""
self.server.stop()
def dispatch_request(self, request):
"""Handle incoming request."""
from werkzeug.exceptions import (
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)
self.server.close()
yield from self.server.wait_closed()
yield from self.app.shutdown()
yield from self._handler.finish_connections(60.0)
yield from self.app.cleanup()
@staticmethod
def get_real_ip(request):
"""Return the clients correct ip address, even in proxied setups."""
if request.access_route:
return request.access_route[-1]
else:
return request.remote_addr
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks."""
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):
"""Base view for all views."""
url = None
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
def __init__(self, hass):
"""Initilalize the base view."""
from werkzeug.wrappers import Response
if not hasattr(self, 'url'):
class_name = self.__class__.__name__
raise AttributeError(
@ -472,59 +403,99 @@ class HomeAssistantView(object):
)
self.hass = hass
# pylint: disable=invalid-name
self.Response = Response
def handle_request(self, request, **values):
"""Handle request to url."""
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
def json(self, result, status_code=200): # pylint: disable=no-self-use
"""Return a JSON response."""
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":
# For CORS preflight requests.
return self.options(request)
def json_message(self, error, status_code=200):
"""Return a JSON message response."""
return self.json({'message': error}, status_code)
try:
handler = getattr(self, request.method.lower())
except AttributeError:
raise MethodNotAllowed
@asyncio.coroutine
def file(self, request, fil): # pylint: disable=no-self-use
"""Return a file."""
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)
# Auth code verbose on purpose
authenticated = False
if self.hass.wsgi.api_password is None:
if view.hass.http.api_password is None:
authenticated = True
elif self.hass.wsgi.is_trusted_ip(remote_addr):
elif view.hass.http.is_trusted_ip(remote_addr):
authenticated = True
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
authenticated = True
elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''),
self.hass.wsgi.api_password):
elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
view.hass.http.api_password):
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 '
'password from %s', remote_addr)
persistent_notification.create(
self.hass,
persistent_notification.async_create(
view.hass,
'Invalid password used from {}'.format(remote_addr),
'Login attempt failed', NOTIFICATION_ID_LOGIN)
raise Unauthorized()
raise HTTPUnauthorized()
request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)',
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
return result
@ -533,36 +504,14 @@ class HomeAssistantView(object):
if isinstance(result, tuple):
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 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)
return web.Response(body=result, status=status_code)
def json_message(self, error, status_code=200):
"""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)
return handle

View File

@ -247,7 +247,7 @@ def setup(hass, 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, {})
hass.wsgi.register_view(iOSPushConfigView(hass,

View File

@ -11,6 +11,7 @@ from itertools import groupby
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
@ -19,7 +20,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
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.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):
"""Listen for download events to download files."""
@asyncio.coroutine
@callback
def log_message(service):
"""Handle sending notification message service calls."""
message = service.data[ATTR_MESSAGE]
@ -100,7 +101,7 @@ def setup(hass, config):
message = message.async_render()
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',
'mdi:format-list-bulleted-type')
@ -115,24 +116,37 @@ class LogbookView(HomeAssistantView):
url = '/api/logbook'
name = 'api:logbook'
extra_urls = ['/api/logbook/<datetime:datetime>']
extra_urls = ['/api/logbook/{datetime}']
def __init__(self, hass, config):
"""Initilalize the logbook view."""
super().__init__(hass)
self.config = config
@asyncio.coroutine
def get(self, request, datetime=None):
"""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)
events = recorder.get_model('Events')
query = recorder.query('Events').filter(
(events.time_fired > start_day) &
(events.time_fired < end_day))
events = recorder.execute(query)
events = _exclude_events(events, self.config)
def get_results():
"""Query DB for results."""
events = recorder.get_model('Events')
query = recorder.query('Events').filter(
(events.time_fired > start_day) &
(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))

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
https://home-assistant.io/components/media_player/
"""
import asyncio
import hashlib
import logging
import os
import requests
from aiohttp import web
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
@ -291,7 +293,7 @@ def setup(hass, config):
component = EntityComponent(
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)
@ -677,7 +679,7 @@ class MediaPlayerImageView(HomeAssistantView):
"""Media player view to serve an image."""
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"
def __init__(self, hass, entities):
@ -685,26 +687,34 @@ class MediaPlayerImageView(HomeAssistantView):
super().__init__(hass)
self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id):
"""Start a get request."""
player = self.entities.get(entity_id)
if player is None:
return self.Response(status=404)
return web.Response(status=404)
authenticated = (request.authenticated or
request.args.get('token') == player.access_token)
request.GET.get('token') == player.access_token)
if not authenticated:
return self.Response(status=401)
return web.Response(status=401)
image_url = player.media_image_url
if image_url:
response = requests.get(image_url)
else:
response = None
if image_url is None:
return web.Response(status=404)
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:
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
https://home-assistant.io/components/notify.html5/
"""
import asyncio
import os
import logging
import json
@ -107,9 +108,9 @@ def get_service(hass, config):
if registrations is None:
return None
hass.wsgi.register_view(
hass.http.register_view(
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_sender_id = config.get(ATTR_GCM_SENDER_ID)
@ -163,12 +164,18 @@ class HTML5PushRegistrationView(HomeAssistantView):
self.registrations = registrations
self.json_path = json_path
@asyncio.coroutine
def post(self, request):
"""Accept the POST request for push registrations from a browser."""
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:
return self.json_message(humanize_error(request.json, ex),
return self.json_message(humanize_error(data, ex),
HTTP_BAD_REQUEST)
name = ensure_unique_string('unnamed device',
@ -182,9 +189,15 @@ class HTML5PushRegistrationView(HomeAssistantView):
return self.json_message('Push notification subscriber registered.')
@asyncio.coroutine
def delete(self, request):
"""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
@ -270,23 +283,29 @@ class HTML5PushCallbackView(HomeAssistantView):
status_code=HTTP_UNAUTHORIZED)
return payload
@asyncio.coroutine
def post(self, request):
"""Accept the POST request for push registrations event callback."""
auth_check = self.check_authorization_header(request)
if not isinstance(auth_check, dict):
return auth_check
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
event_payload = {
ATTR_TAG: request.json.get(ATTR_TAG),
ATTR_TYPE: request.json[ATTR_TYPE],
ATTR_TAG: data.get(ATTR_TAG),
ATTR_TYPE: data[ATTR_TYPE],
ATTR_TARGET: auth_check[ATTR_TARGET],
}
if request.json.get(ATTR_ACTION) is not None:
event_payload[ATTR_ACTION] = request.json.get(ATTR_ACTION)
if data.get(ATTR_ACTION) is not None:
event_payload[ATTR_ACTION] = data.get(ATTR_ACTION)
if request.json.get(ATTR_DATA) is not None:
event_payload[ATTR_DATA] = request.json.get(ATTR_DATA)
if data.get(ATTR_DATA) is not None:
event_payload[ATTR_DATA] = data.get(ATTR_DATA)
try:
event_payload = CALLBACK_EVENT_PAYLOAD_SCHEMA(event_payload)

View File

@ -153,7 +153,7 @@ def setup(hass, config):
# Create Alpr device / render engine
if render == RENDER_FFMPEG:
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)
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
https://home-assistant.io/components/persistent_notification/
"""
import asyncio
import os
import logging
@ -14,6 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'persistent_notification'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -35,6 +37,14 @@ _LOGGER = logging.getLogger(__name__)
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."""
data = {
key: value for key, value in [
@ -44,7 +54,7 @@ def create(hass, message, title=None, notification_id=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):

View File

@ -12,6 +12,7 @@ import time
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import PLATFORM_SCHEMA
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',
'settings', 'sleep', 'weight'])
hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
hass.wsgi.register_view(FitbitAuthCallbackView(
hass.http.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
hass.http.register_view(FitbitAuthCallbackView(
hass, config, add_devices, oauth))
request_oauth_completion(hass)
@ -294,12 +295,13 @@ class FitbitAuthCallbackView(HomeAssistantView):
self.add_devices = add_devices
self.oauth = oauth
@callback
def get(self, request):
"""Finish OAuth callback request."""
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
data = request.args
data = request.GET
response_message = """Fitbit has been successfully authorized!
You can close this window now!"""
@ -340,7 +342,8 @@ class FitbitAuthCallbackView(HomeAssistantView):
config_contents):
_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

View File

@ -9,6 +9,7 @@ import re
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.sensor import PLATFORM_SCHEMA
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)
sensors = {}
hass.wsgi.register_view(TorqueReceiveDataView(
hass.http.register_view(TorqueReceiveDataView(
hass, email, vehicle, sensors, add_devices))
return True
@ -77,9 +78,10 @@ class TorqueReceiveDataView(HomeAssistantView):
self.sensors = sensors
self.add_devices = add_devices
@callback
def get(self, request):
"""Handle Torque data request."""
data = request.args
data = request.GET
if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]:
return
@ -100,14 +102,14 @@ class TorqueReceiveDataView(HomeAssistantView):
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in self.sensors:
self.sensors[pid].on_update(data[key])
self.sensors[pid].async_on_update(data[key])
for pid in names:
if pid not in self.sensors:
self.sensors[pid] = TorqueSensor(
ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]),
units.get(pid, None))
self.add_devices([self.sensors[pid]])
self.hass.async_add_job(self.add_devices, [self.sensors[pid]])
return None
@ -141,7 +143,8 @@ class TorqueSensor(Entity):
"""Return the default icon of the sensor."""
return 'mdi:car'
def on_update(self, value):
@callback
def async_on_update(self, value):
"""Receive an update."""
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
from homeassistant.core import callback
from homeassistant import util
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
@ -40,7 +41,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
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({
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)
if len(DEVICES) == 0:
hass.wsgi.register_view(NetioApiView)
hass.http.register_view(NetioApiView)
dev = Netio(host, port, username, password)
@ -93,9 +94,10 @@ class NetioApiView(HomeAssistantView):
url = URL_API_NETIO_EP
name = 'api:netio'
@callback
def get(self, request, host):
"""Request handler."""
data = request.args
data = request.GET
states, consumptions, cumulated_consumptions, start_dates = \
[], [], [], []
@ -117,7 +119,7 @@ class NetioApiView(HomeAssistantView):
ndev.start_dates = start_dates
for dev in DEVICES[host].entities:
dev.update_ha_state()
self.hass.loop.create_task(dev.async_update_ha_state())
return self.json(True)

View File

@ -83,12 +83,14 @@ SERVICE_TO_STATE = {
# 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.
Add all states that have changed since the start time to the return list
when with-block is exited.
Must be run within the event loop.
"""
def __init__(self, hass):
@ -103,7 +105,8 @@ class TrackStates(object):
def __exit__(self, exc_type, exc_value, traceback):
"""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):

View File

@ -213,35 +213,35 @@ class EventForwarder(object):
self._targets = {}
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.
Will overwrite old target if one exists with same host/port.
"""
with self._lock:
if self._unsub_listener is None:
self._unsub_listener = self.hass.bus.listen(
ha.MATCH_ALL, self._event_listener)
if self._async_unsub_listener is None:
self._async_unsub_listener = self.hass.bus.async_listen(
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."""
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:
# Remove event listener if no forwarding targets present
self._unsub_listener()
self._unsub_listener = None
if len(self._targets) == 0:
# Remove event listener if no forwarding targets present
self._async_unsub_listener()
self._async_unsub_listener = None
return did_remove
return did_remove
def _event_listener(self, event):
"""Listen and forward all events."""

View File

@ -6,6 +6,8 @@ pip>=7.0.0
jinja2>=2.8
voluptuous==0.9.2
typing>=3,<4
aiohttp==1.0.5
async_timeout==1.0.0
# homeassistant.components.nuimo_controller
--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
TwitterAPI==2.4.2
# homeassistant.components.emulated_hue
# homeassistant.components.http
Werkzeug==0.11.11
aiohttp_cors==0.4.0
# homeassistant.components.apcupsd
apcaccess==0.0.4
@ -62,10 +63,6 @@ blockchain==1.3.3
# homeassistant.components.notify.aws_sqs
boto3==1.3.1
# homeassistant.components.emulated_hue
# homeassistant.components.http
cherrypy==8.1.2
# homeassistant.components.sensor.coinmarketcap
coinmarketcap==2.0.1
@ -136,7 +133,7 @@ gps3==0.33.3
ha-alpr==0.3
# homeassistant.components.ffmpeg
ha-ffmpeg==0.13
ha-ffmpeg==0.14
# homeassistant.components.mqtt.server
hbmqtt==0.7.1
@ -483,10 +480,6 @@ speedtest-cli==0.3.4
# homeassistant.scripts.db_migrator
sqlalchemy==1.1.1
# homeassistant.components.emulated_hue
# homeassistant.components.http
static3==0.7.0
# homeassistant.components.statsd
statsd==3.2.1

View File

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

View File

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

View File

@ -1,29 +1 @@
"""Setup some common test helper things."""
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'
"""Tests for Home Assistant."""

View File

@ -38,23 +38,11 @@ def get_test_home_assistant(num_threads=None):
orig_num_threads = ha.MIN_WORKER_THREAD
ha.MIN_WORKER_THREAD = num_threads
hass = ha.HomeAssistant(loop)
hass = loop.run_until_complete(async_test_home_assistant(loop))
if 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
stop_event = threading.Event()
@ -98,6 +86,35 @@ def get_test_home_assistant(num_threads=None):
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():
"""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):
"""Mock the HTTP component."""
hass.wsgi = mock.MagicMock()
hass.http = mock.MagicMock()
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):

View File

@ -1,36 +1,18 @@
"""The tests for generic camera component."""
import unittest
import asyncio
from unittest import mock
import requests_mock
from werkzeug.test import EnvironBuilder
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):
"""Test the generic camera platform."""
@asyncio.coroutine
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):
"""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):
"""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', {
def setup_platform():
"""Setup the platform."""
assert setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'generic',
@ -39,32 +21,32 @@ class TestGenericCamera(unittest.TestCase):
'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')
Request = request_class()
request = Request(builder.get_environ())
request.authenticated = True
resp = image_view.get(request, 'camera.config_test')
client = yield from test_client(hass.http.app)
assert m.call_count == 1
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello world'
resp = yield from client.get('/api/camera_proxy/camera.config_test')
image_view.get(request, 'camera.config_test')
assert m.call_count == 2
assert aioclient_mock.call_count == 1
assert resp.status == 200
body = yield from resp.text()
assert body == 'hello world'
@requests_mock.Mocker()
def test_limit_refetch(self, m):
"""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)
resp = yield from client.get('/api/camera_proxy/camera.config_test')
assert aioclient_mock.call_count == 2
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': {
'name': 'config_test',
'platform': 'generic',
@ -73,43 +55,47 @@ class TestGenericCamera(unittest.TestCase):
'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')
Request = request_class()
request = Request(builder.get_environ())
request.authenticated = True
client = yield from test_client(hass.http.app)
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()):
resp = image_view.get(request, 'camera.config_test')
assert m.call_count == 0
assert resp.status_code == 500, resp.response
hass.states.async_set('sensor.temp', '5')
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')
assert m.call_count == 1
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello world'
hass.states.async_set('sensor.temp', '10')
resp = image_view.get(request, 'camera.config_test')
assert m.call_count == 1
assert resp.status_code == 200, resp.response
assert resp.response[0].decode('utf-8') == 'hello world'
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'
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
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'
hass.states.async_set('sensor.temp', '15')
# Cause a template render error
self.hass.states.remove('sensor.temp')
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'
# Url change = fetch new image
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'
# 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."""
import unittest
import asyncio
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.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):
"""Test the local file camera component."""
@asyncio.coroutine
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):
"""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')
yield from hass.loop.run_in_executor(None, setup_platform)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
client = yield from test_client(hass.http.app)
def test_loading_file(self):
"""Test that it loads image from disk."""
test_string = 'hello'
self.hass.wsgi = mock.MagicMock()
m_open = MockOpen(read_data=b'hello')
with mock.patch(
'homeassistant.components.camera.local_file.open',
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)), \
mock.patch('os.access', mock.Mock(return_value=True)):
assert setup_component(self.hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'local_file',
'file_path': 'mock.file',
}})
assert resp.status == 200
body = yield from resp.text()
assert body == 'hello'
image_view = self.hass.wsgi.mock_calls[0][1][0]
m_open = mock.mock_open(read_data=test_string)
with mock.patch(
'homeassistant.components.camera.local_file.open',
m_open, create=True
):
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()
@asyncio.coroutine
def test_file_not_readable(hass):
"""Test local file will not setup when file is not readable."""
mock_http_component(hass)
def run_test():
with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \
mock.patch('os.access', return_value=False), \
assert_setup_component(0):
assert setup_component(self.hass, 'camera', {
assert_setup_component(0, 'camera'):
assert setup_component(hass, 'camera', {
'camera': {
'name': 'config_test',
'platform': 'local_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):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.wsgi = mock.MagicMock()
self.hass.http = mock.MagicMock()
self.hass.config.components = ['http']
def tearDown(self):

View File

@ -18,42 +18,19 @@ HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT)
API_PASSWORD = "test1234"
HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD}
hass = None
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):
"""Test the media_player module."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = hass
try:
self.hass.config.components.remove(mp.DOMAIN)
except ValueError:
pass
self.hass = get_test_home_assistant()
def tearDown(self):
"""Shut down test instance."""
self.hass.stop()
def test_source_select(self):
"""Test the input source service."""
@ -226,21 +203,6 @@ class TestDemoMediaPlayer(unittest.TestCase):
assert 0 == (mp.SUPPORT_PREVIOUS_TRACK &
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.'
'media_seek')
def test_play_media(self, mock_seek):
@ -275,3 +237,42 @@ class TestDemoMediaPlayer(unittest.TestCase):
mp.media_seek(self.hass, 100, ent_id)
self.hass.block_till_done()
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."""
import asyncio
import json
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
SUBSCRIPTION_1 = {
@ -35,6 +35,9 @@ SUBSCRIPTION_3 = {
},
}
REGISTER_URL = '/api/notify.html5'
PUBLISH_URL = '/api/notify.html5/callback'
class TestHtml5Notify(object):
"""Tests for HTML5 notify platform."""
@ -94,9 +97,13 @@ class TestHtml5Notify(object):
assert payload['body'] == 'Hello'
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."""
hass = MagicMock()
expected = {
'unnamed device': SUBSCRIPTION_1,
}
m = mock_open()
with patch(
@ -114,21 +121,20 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value
assert view.registrations == {}
builder = EnvironBuilder(method='POST',
data=json.dumps(SUBSCRIPTION_1))
Request = request_class()
resp = view.post(Request(builder.get_environ()))
app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_1))
expected = {
'unnamed device': SUBSCRIPTION_1,
}
assert resp.status_code == 200, resp.response
content = yield from resp.text()
assert resp.status == 200, content
assert view.registrations == expected
handle = m()
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."""
hass = MagicMock()
@ -146,34 +152,34 @@ class TestHtml5Notify(object):
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',
'subscription': 'sub info',
}))
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 400, resp.response
assert resp.status == 400
builder = EnvironBuilder(method='POST', data=json.dumps({
resp = yield from client.post(REGISTER_URL, data=json.dumps({
'browser': 'chrome',
}))
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 400, resp.response
assert resp.status == 400
builder = EnvironBuilder(method='POST', data=json.dumps({
'browser': 'chrome',
'subscription': 'sub info',
}))
with patch('homeassistant.components.notify.html5._save_config',
return_value=False):
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 400, resp.response
# resp = view.post(Request(builder.get_environ()))
resp = yield from client.post(REGISTER_URL, data=json.dumps({
'browser': 'chrome',
'subscription': 'sub info',
}))
@patch('homeassistant.components.notify.html5.os')
def test_unregistering_device_view(self, mock_os):
assert resp.status == 400
@asyncio.coroutine
def test_unregistering_device_view(self, loop, test_client):
"""Test that the HTML unregister view works."""
mock_os.path.isfile.return_value = True
hass = MagicMock()
config = {
@ -182,11 +188,14 @@ class TestHtml5Notify(object):
}
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'
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
@ -197,23 +206,25 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value
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'],
}))
Request = request_class()
resp = view.delete(Request(builder.get_environ()))
config.pop('some device')
assert resp.status_code == 200, resp.response
assert resp.status == 200, resp.response
assert view.registrations == config
handle = m()
assert json.loads(handle.write.call_args[0][0]) == config
@patch('homeassistant.components.notify.html5.os')
def test_unregister_device_view_handle_unknown_subscription(self, mock_os):
@asyncio.coroutine
def test_unregister_device_view_handle_unknown_subscription(self, loop,
test_client):
"""Test that the HTML unregister view handles unknown subscriptions."""
mock_os.path.isfile.return_value = True
hass = MagicMock()
config = {
@ -226,7 +237,9 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True
):
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
@ -237,21 +250,23 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value
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']
}))
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
handle = m()
assert handle.write.call_count == 0
@patch('homeassistant.components.notify.html5.os')
def test_unregistering_device_view_handles_json_safe_error(self, mock_os):
@asyncio.coroutine
def test_unregistering_device_view_handles_json_safe_error(self, loop,
test_client):
"""Test that the HTML unregister view handles JSON write errors."""
mock_os.path.isfile.return_value = True
hass = MagicMock()
config = {
@ -264,7 +279,9 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True
):
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
@ -275,21 +292,23 @@ class TestHtml5Notify(object):
assert view.json_path == hass.config.path.return_value
assert view.registrations == config
builder = EnvironBuilder(method='DELETE', data=json.dumps({
'subscription': SUBSCRIPTION_1['subscription'],
}))
Request = request_class()
app = web.Application(loop=loop)
view.register(app.router)
client = yield from test_client(app)
with patch('homeassistant.components.notify.html5._save_config',
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
handle = m()
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."""
hass = MagicMock()
@ -307,20 +326,20 @@ class TestHtml5Notify(object):
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',
'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')
@patch('pywebpush.WebPusher')
def test_callback_view_with_jwt(self, mock_wp, mock_os):
@asyncio.coroutine
def test_callback_view_with_jwt(self, loop, test_client):
"""Test that the notification callback view works with JWT."""
mock_os.path.isfile.return_value = True
hass = MagicMock()
data = {
@ -332,15 +351,18 @@ class TestHtml5Notify(object):
'homeassistant.components.notify.html5.open', m, create=True
):
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 hass.called
assert len(hass.mock_calls) == 3
service.send_message('Hello', target=['device'],
data={'icon': 'beer.png'})
with patch('pywebpush.WebPusher') as mock_wp:
service.send_message('Hello', target=['device'],
data={'icon': 'beer.png'})
assert len(mock_wp.mock_calls) == 2
@ -359,13 +381,14 @@ class TestHtml5Notify(object):
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',
}), headers={'Authorization': bearer_token})
Request = request_class()
resp = view.post(Request(builder.get_environ()))
assert resp.status_code == 200, resp.response
returned = resp.response[0].decode('utf-8')
expected = '{"event": "push", "status": "ok"}'
assert json.loads(returned) == json.loads(expected)
assert resp.status == 200
body = yield from resp.json()
assert body == {"event": "push", "status": "ok"}

View File

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

View File

@ -1,11 +1,13 @@
"""The tests for the Home Assistant API component."""
# pylint: disable=protected-access,too-many-public-methods
import asyncio
from contextlib import closing
import json
import time
import unittest
from unittest.mock import Mock, patch
from aiohttp import web
import requests
from homeassistant import bootstrap, const
@ -243,20 +245,18 @@ class TestAPI(unittest.TestCase):
def test_api_get_error_log(self):
"""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
# bug in read_data, it can't handle byte types ('Type str doesn't
# support the buffer API'), but wsgiserver requires byte types
# ('WSGI Applications must yield bytes'). So just mock our own
# read method.
m_open = Mock(return_value=Mock(
read=Mock(side_effect=[test_string]))
)
with patch('homeassistant.components.http.open', m_open, create=True):
@asyncio.coroutine
def mock_send():
"""Mock file send."""
return web.Response(text=test_string)
with patch('homeassistant.components.http.HomeAssistantView.file',
Mock(return_value=mock_send())):
req = requests.get(_url(const.URL_API_ERROR_LOG),
headers=HA_HEADERS)
self.assertEqual(test_string, req.text.encode('UTF-8'))
self.assertEqual(test_string, req.text)
self.assertIsNone(req.headers.get('expires'))
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.states.set('test.test', 'a_state')
bootstrap.setup_component(
assert bootstrap.setup_component(
hass, http.DOMAIN,
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
http.CONF_SERVER_PORT: SERVER_PORT}})
bootstrap.setup_component(hass, 'frontend')
assert bootstrap.setup_component(hass, 'frontend')
hass.start()
time.sleep(0.05)
@ -71,7 +71,7 @@ class TestFrontend(unittest.TestCase):
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)

View File

@ -56,7 +56,7 @@ def setUpModule():
bootstrap.setup_component(hass, 'api')
hass.wsgi.trusted_networks = [
hass.http.trusted_networks = [
ip_network(trusted_network)
for trusted_network in TRUSTED_NETWORKS]
@ -159,12 +159,9 @@ class TestHttp:
headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL})
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.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):
"""Test cross origin resource sharing with password in header."""
@ -175,12 +172,9 @@ class TestHttp:
req = requests.get(_url(const.URL_API), headers=headers)
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.headers.get(allow_origin) == HTTP_BASE_URL
assert req.headers.get(allow_headers) == all_allow_headers
def test_cors_denied_without_origin_header(self):
"""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_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
all_allow_headers = ', '.join(const.ALLOWED_CORS_HEADERS)
assert req.status_code == 200
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."""
import unittest
from unittest import mock
from unittest.mock import patch
import influxdb as influx_client
@ -60,6 +61,8 @@ class TestInfluxDB(unittest.TestCase):
assert setup_component(self.hass, influxdb.DOMAIN, config)
@patch('homeassistant.components.persistent_notification.create',
mock.MagicMock())
def test_setup_missing_password(self, mock_client):
"""Test the setup with existing username and missing password."""
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."""
import asyncio
from datetime import timedelta
import unittest
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
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):
"""Test the Home Assistant event helpers."""
@ -54,31 +91,6 @@ class TestStateHelpers(unittest.TestCase):
[state2, state3],
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):
"""Test reproduce_state with no entity."""
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