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:
parent
9aa88819a5
commit
519d9f2fd0
45 changed files with 1422 additions and 1009 deletions
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue