diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py
deleted file mode 100644
index 7fd187daa4d..00000000000
--- a/homeassistant/components/emulated_hue.py
+++ /dev/null
@@ -1,566 +0,0 @@
-"""
-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 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, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
-)
-from homeassistant.components.light import (
- ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
-)
-from homeassistant.components.http import (
- HomeAssistantView, HomeAssistantWSGI
-)
-import homeassistant.helpers.config_validation as cv
-
-DOMAIN = 'emulated_hue'
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_HOST_IP = 'host_ip'
-CONF_LISTEN_PORT = 'listen_port'
-CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
-CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
-CONF_EXPOSED_DOMAINS = 'exposed_domains'
-
-ATTR_EMULATED_HUE = 'emulated_hue'
-ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
-
-DEFAULT_LISTEN_PORT = 8300
-DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
-DEFAULT_EXPOSE_BY_DEFAULT = True
-DEFAULT_EXPOSED_DOMAINS = [
- 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
-]
-
-HUE_API_STATE_ON = 'on'
-HUE_API_STATE_BRI = 'bri'
-
-CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
- vol.Optional(CONF_HOST_IP): cv.string,
- vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
- vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
- vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
- vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
- vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list
- })
-}, extra=vol.ALLOW_EXTRA)
-
-
-def setup(hass, yaml_config):
- """Activate the emulated_hue component."""
- config = Config(yaml_config)
-
- server = HomeAssistantWSGI(
- hass,
- development=False,
- server_host=config.host_ip_addr,
- server_port=config.listen_port,
- api_password=None,
- ssl_certificate=None,
- ssl_key=None,
- cors_origins=None,
- use_x_forwarded_for=False,
- trusted_networks=[],
- login_threshold=0,
- is_ban_enabled=False
- )
-
- server.register_view(DescriptionXmlView(config))
- server.register_view(HueUsernameView)
- server.register_view(HueLightsView(config))
-
- upnp_listener = UPNPResponderThread(
- config.host_ip_addr, config.listen_port)
-
- @asyncio.coroutine
- def stop_emulated_hue_bridge(event):
- """Stop the emulated hue bridge."""
- upnp_listener.stop()
- yield from server.stop()
-
- @asyncio.coroutine
- def start_emulated_hue_bridge(event):
- """Start the emulated hue bridge."""
- upnp_listener.start()
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
- stop_emulated_hue_bridge)
- yield from server.start()
-
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
-
- return True
-
-
-class Config(object):
- """Holds configuration variables for the emulated hue bridge."""
-
- def __init__(self, yaml_config):
- """Initialize the instance."""
- conf = yaml_config.get(DOMAIN, {})
-
- # Get the IP address that will be passed to the Echo during discovery
- self.host_ip_addr = conf.get(CONF_HOST_IP)
- if self.host_ip_addr is None:
- self.host_ip_addr = util.get_local_ip()
- _LOGGER.warning(
- "Listen IP address not specified, auto-detected address is %s",
- self.host_ip_addr)
-
- # Get the port that the Hue bridge will listen on
- self.listen_port = conf.get(CONF_LISTEN_PORT)
- if not isinstance(self.listen_port, int):
- self.listen_port = DEFAULT_LISTEN_PORT
- _LOGGER.warning(
- "Listen port not specified, defaulting to %s",
- self.listen_port)
-
- # Get domains that cause both "on" and "off" commands to map to "on"
- # This is primarily useful for things like scenes or scripts, which
- # don't really have a concept of being off
- self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
- if not isinstance(self.off_maps_to_on_domains, list):
- self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
-
- # Get whether or not entities should be exposed by default, or if only
- # explicitly marked ones will be exposed
- self.expose_by_default = conf.get(
- CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
-
- # Get domains that are exposed by default when expose_by_default is
- # True
- self.exposed_domains = conf.get(
- CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
-
-
-class DescriptionXmlView(HomeAssistantView):
- """Handles requests for the description.xml file."""
-
- url = '/description.xml'
- name = 'description:xml'
- requires_auth = False
-
- def __init__(self, config):
- """Initialize the instance of the view."""
- self.config = config
-
- @core.callback
- def get(self, request):
- """Handle a GET request."""
- xml_template = """
-
-
-1
-0
-
-http://{0}:{1}/
-
-urn:schemas-upnp-org:device:Basic:1
-HASS Bridge ({0})
-Royal Philips Electronics
-http://www.philips.com
-Philips hue Personal Wireless Lighting
-Philips hue bridge 2015
-BSB002
-http://www.meethue.com
-1234
-uuid:2f402f80-da50-11e1-9b23-001788255acc
-
-
-"""
-
- resp_text = xml_template.format(
- self.config.host_ip_addr, self.config.listen_port)
-
- return web.Response(text=resp_text, content_type='text/xml')
-
-
-class HueUsernameView(HomeAssistantView):
- """Handle requests to create a username for the emulated hue bridge."""
-
- url = '/api'
- name = 'hue:api'
- extra_urls = ['/api/']
- requires_auth = False
-
- @asyncio.coroutine
- def post(self, request):
- """Handle a POST request."""
- 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',
- HTTP_BAD_REQUEST)
-
- return self.json([{'success': {'username': '12345678901234567890'}}])
-
-
-class HueLightsView(HomeAssistantView):
- """Handle requests for getting and setting info about entities."""
-
- url = '/api/{username}/lights'
- name = 'api:username:lights'
- extra_urls = ['/api/{username}/lights/{entity_id}',
- '/api/{username}/lights/{entity_id}/state']
- requires_auth = False
-
- def __init__(self, config):
- """Initialize the instance of the view."""
- self.config = config
- self.cached_states = {}
-
- @core.callback
- def get(self, request, username, entity_id=None):
- """Handle a GET request."""
- hass = request.app['hass']
-
- if entity_id is None:
- return self.async_get_lights_list(hass)
-
- if not request.path.endswith('state'):
- return self.async_get_light_state(hass, entity_id)
-
- return web.Response(text="Method not allowed", status=405)
-
- @asyncio.coroutine
- def put(self, request, username, entity_id=None):
- """Handle a PUT request."""
- hass = request.app['hass']
-
- if not request.path.endswith('state'):
- return web.Response(text="Method not allowed", status=405)
-
- if entity_id and hass.states.get(entity_id) is None:
- return self.json_message('Entity not found', HTTP_NOT_FOUND)
-
- try:
- json_data = yield from request.json()
- except ValueError:
- return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
-
- result = yield from self.async_put_light_state(hass, json_data,
- entity_id)
- return result
-
- @core.callback
- def async_get_lights_list(self, hass):
- """Process a request to get the list of available lights."""
- json_response = {}
-
- for entity in hass.states.async_all():
- if self.is_entity_exposed(entity):
- json_response[entity.entity_id] = entity_to_json(entity)
-
- return self.json(json_response)
-
- @core.callback
- def async_get_light_state(self, hass, entity_id):
- """Process a request to get the state of an individual light."""
- entity = hass.states.get(entity_id)
- if entity is None or not self.is_entity_exposed(entity):
- return web.Response(text="Entity not found", status=404)
-
- cached_state = self.cached_states.get(entity_id, None)
-
- if cached_state is None:
- final_state = entity.state == STATE_ON
- final_brightness = entity.attributes.get(
- ATTR_BRIGHTNESS, 255 if final_state else 0)
- else:
- final_state, final_brightness = cached_state
-
- json_response = entity_to_json(entity, final_state, final_brightness)
-
- return self.json(json_response)
-
- @asyncio.coroutine
- def async_put_light_state(self, hass, 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 = hass.states.get(entity_id)
- if entity is None:
- return web.Response(text="Entity not found", status=404)
-
- if not self.is_entity_exposed(entity):
- 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 web.Response(text="Bad request", status=400)
-
- result, brightness = parsed
-
- # Convert the resulting "on" status into the service we need to call
- service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
-
- # Construct what we need to send to the service
- data = {ATTR_ENTITY_ID: entity_id}
-
- # If the requested entity is a script add some variables
- if entity.domain.lower() == "script":
- data['variables'] = {
- 'requested_state': STATE_ON if result else STATE_OFF
- }
-
- if brightness is not None:
- data['variables']['requested_level'] = brightness
-
- elif brightness is not None:
- data[ATTR_BRIGHTNESS] = brightness
-
- if entity.domain.lower() in config.off_maps_to_on_domains:
- # Map the off command to on
- service = SERVICE_TURN_ON
-
- # Caching is required because things like scripts and scenes won't
- # report as "off" to Alexa if an "off" command is received, because
- # they'll map to "on". Thus, instead of reporting its actual
- # status, we report what Alexa will want to see, which is the same
- # as the actual requested command.
- self.cached_states[entity_id] = (result, brightness)
-
- # Perform the requested action
- yield from hass.services.async_call(core.DOMAIN, service, data,
- blocking=True)
-
- json_response = \
- [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
-
- if brightness is not None:
- json_response.append(create_hue_success_response(
- entity_id, HUE_API_STATE_BRI, brightness))
-
- return self.json(json_response)
-
- def is_entity_exposed(self, entity):
- """Determine if an entity should be exposed on the emulated bridge.
-
- Async friendly.
- """
- config = self.config
-
- if entity.attributes.get('view') is not None:
- # Ignore entities that are views
- return False
-
- domain = entity.domain.lower()
- explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
-
- domain_exposed_by_default = \
- config.expose_by_default and domain in config.exposed_domains
-
- # Expose an entity if the entity's domain is exposed by default and
- # the configuration doesn't explicitly exclude it from being
- # exposed, or if the entity is explicitly exposed
- is_default_exposed = \
- domain_exposed_by_default and explicit_expose is not False
-
- return is_default_exposed or explicit_expose
-
-
-def parse_hue_api_put_light_body(request_json, entity):
- """Parse the body of a request to change the state of a light."""
- if HUE_API_STATE_ON in request_json:
- if not isinstance(request_json[HUE_API_STATE_ON], bool):
- return None
-
- if request_json['on']:
- # Echo requested device be turned on
- brightness = None
- report_brightness = False
- result = True
- else:
- # Echo requested device be turned off
- brightness = None
- report_brightness = False
- result = False
-
- if HUE_API_STATE_BRI in request_json:
- # Make sure the entity actually supports brightness
- entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
-
- if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
- try:
- # Clamp brightness from 0 to 255
- brightness = \
- max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
- except ValueError:
- return None
-
- report_brightness = True
- result = (brightness > 0)
- elif entity.domain.lower() == "script":
- # Convert 0-255 to 0-100
- level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
-
- brightness = round(level)
- report_brightness = True
- result = True
-
- return (result, brightness) if report_brightness else (result, None)
-
-
-def entity_to_json(entity, is_on=None, brightness=None):
- """Convert an entity to its Hue bridge JSON representation."""
- if is_on is None:
- is_on = entity.state == STATE_ON
-
- if brightness is None:
- brightness = 255 if is_on else 0
-
- name = entity.attributes.get(
- ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
-
- return {
- 'state':
- {
- HUE_API_STATE_ON: is_on,
- HUE_API_STATE_BRI: brightness,
- 'reachable': True
- },
- 'type': 'Dimmable light',
- 'name': name,
- 'modelid': 'HASS123',
- 'uniqueid': entity.entity_id,
- 'swversion': '123'
- }
-
-
-def create_hue_success_response(entity_id, attr, value):
- """Create a success response for an attribute set on a light."""
- success_key = '/lights/{}/state/{}'.format(entity_id, attr)
- return {'success': {success_key: value}}
-
-
-class UPNPResponderThread(threading.Thread):
- """Handle responding to UPNP/SSDP discovery requests."""
-
- _interrupted = False
-
- def __init__(self, host_ip_addr, listen_port):
- """Initialize the class."""
- threading.Thread.__init__(self)
-
- self.host_ip_addr = host_ip_addr
- self.listen_port = listen_port
-
- # Note that the double newline at the end of
- # this string is required per the SSDP spec
- resp_template = """HTTP/1.1 200 OK
-CACHE-CONTROL: max-age=60
-EXT:
-LOCATION: http://{0}:{1}/description.xml
-SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
-ST: urn:schemas-upnp-org:device:basic:1
-USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
-
-"""
-
- self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
- .replace("\n", "\r\n") \
- .encode('utf-8')
-
- # Set up a pipe for signaling to the receiver that it's time to
- # shutdown. Essentially, we place the SSDP socket into nonblocking
- # mode and use select() to wait for data to arrive on either the SSDP
- # socket or the pipe. If data arrives on either one, select() returns
- # and tells us which filenos have data ready to read.
- #
- # When we want to stop the responder, we write data to the pipe, which
- # causes the select() to return and indicate that said pipe has data
- # ready to be read, which indicates to us that the responder needs to
- # be shutdown.
- self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
-
- def run(self):
- """Run the server."""
- # Listen for UDP port 1900 packets sent to SSDP multicast address
- ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- ssdp_socket.setblocking(False)
-
- # Required for receiving multicast
- ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-
- ssdp_socket.setsockopt(
- socket.SOL_IP,
- socket.IP_MULTICAST_IF,
- socket.inet_aton(self.host_ip_addr))
-
- ssdp_socket.setsockopt(
- socket.SOL_IP,
- socket.IP_ADD_MEMBERSHIP,
- socket.inet_aton("239.255.255.250") +
- socket.inet_aton(self.host_ip_addr))
-
- ssdp_socket.bind(("239.255.255.250", 1900))
-
- while True:
- if self._interrupted:
- clean_socket_close(ssdp_socket)
- return
-
- try:
- read, _, _ = select.select(
- [self._interrupted_read_pipe, ssdp_socket], [],
- [ssdp_socket])
-
- if self._interrupted_read_pipe in read:
- # Implies self._interrupted is True
- clean_socket_close(ssdp_socket)
- return
- elif ssdp_socket in read:
- data, addr = ssdp_socket.recvfrom(1024)
- else:
- continue
- except socket.error as ex:
- if self._interrupted:
- clean_socket_close(ssdp_socket)
- return
-
- _LOGGER.error("UPNP Responder socket exception occured: %s",
- ex.__str__)
-
- if "M-SEARCH" in data.decode('utf-8'):
- # SSDP M-SEARCH method received, respond to it with our info
- resp_socket = socket.socket(
- socket.AF_INET, socket.SOCK_DGRAM)
-
- resp_socket.sendto(self.upnp_response, addr)
- resp_socket.close()
-
- def stop(self):
- """Stop the server."""
- # Request for server
- self._interrupted = True
- os.write(self._interrupted_write_pipe, bytes([0]))
- self.join()
-
-
-def clean_socket_close(sock):
- """Close a socket connection and logs its closure."""
- _LOGGER.info("UPNP responder shutting down.")
-
- sock.close()
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
new file mode 100644
index 00000000000..2efce06528d
--- /dev/null
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -0,0 +1,198 @@
+"""
+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 logging
+
+import voluptuous as vol
+
+from homeassistant import util
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
+)
+from homeassistant.components.http import HomeAssistantWSGI
+import homeassistant.helpers.config_validation as cv
+from .hue_api import (
+ HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
+ HueOneLightChangeView)
+from .upnp import DescriptionXmlView, UPNPResponderThread
+
+DOMAIN = 'emulated_hue'
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_HOST_IP = 'host_ip'
+CONF_LISTEN_PORT = 'listen_port'
+CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
+CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
+CONF_EXPOSED_DOMAINS = 'exposed_domains'
+CONF_TYPE = 'type'
+
+TYPE_ALEXA = 'alexa'
+TYPE_GOOGLE = 'google_home'
+
+DEFAULT_LISTEN_PORT = 8300
+DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
+DEFAULT_EXPOSE_BY_DEFAULT = True
+DEFAULT_EXPOSED_DOMAINS = [
+ 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
+]
+DEFAULT_TYPE = TYPE_ALEXA
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_HOST_IP): cv.string,
+ vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
+ vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
+ vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
+ vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
+ vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
+ vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
+ vol.Any(TYPE_ALEXA, TYPE_GOOGLE)
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+ATTR_EMULATED_HUE = 'emulated_hue'
+
+
+def setup(hass, yaml_config):
+ """Activate the emulated_hue component."""
+ config = Config(yaml_config.get(DOMAIN, {}))
+
+ server = HomeAssistantWSGI(
+ hass,
+ development=False,
+ server_host=config.host_ip_addr,
+ server_port=config.listen_port,
+ api_password=None,
+ ssl_certificate=None,
+ ssl_key=None,
+ cors_origins=None,
+ use_x_forwarded_for=False,
+ trusted_networks=[],
+ login_threshold=0,
+ is_ban_enabled=False
+ )
+
+ server.register_view(DescriptionXmlView(config))
+ server.register_view(HueUsernameView)
+ server.register_view(HueAllLightsStateView(config))
+ server.register_view(HueOneLightStateView(config))
+ server.register_view(HueOneLightChangeView(config))
+
+ upnp_listener = UPNPResponderThread(
+ config.host_ip_addr, config.listen_port)
+
+ @asyncio.coroutine
+ def stop_emulated_hue_bridge(event):
+ """Stop the emulated hue bridge."""
+ upnp_listener.stop()
+ yield from server.stop()
+
+ @asyncio.coroutine
+ def start_emulated_hue_bridge(event):
+ """Start the emulated hue bridge."""
+ upnp_listener.start()
+ yield from server.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
+
+
+class Config(object):
+ """Holds configuration variables for the emulated hue bridge."""
+
+ def __init__(self, conf):
+ """Initialize the instance."""
+ self.type = conf.get(CONF_TYPE)
+ self.numbers = {}
+ self.cached_states = {}
+
+ # Get the IP address that will be passed to the Echo during discovery
+ self.host_ip_addr = conf.get(CONF_HOST_IP)
+ if self.host_ip_addr is None:
+ self.host_ip_addr = util.get_local_ip()
+ _LOGGER.warning(
+ "Listen IP address not specified, auto-detected address is %s",
+ self.host_ip_addr)
+
+ # Get the port that the Hue bridge will listen on
+ self.listen_port = conf.get(CONF_LISTEN_PORT)
+ if not isinstance(self.listen_port, int):
+ self.listen_port = DEFAULT_LISTEN_PORT
+ _LOGGER.warning(
+ "Listen port not specified, defaulting to %s",
+ self.listen_port)
+
+ if self.type == TYPE_GOOGLE and self.listen_port != 80:
+ _LOGGER.warning('When targetting Google Home, listening port has '
+ 'to be port 80')
+
+ # Get domains that cause both "on" and "off" commands to map to "on"
+ # This is primarily useful for things like scenes or scripts, which
+ # don't really have a concept of being off
+ self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
+ if not isinstance(self.off_maps_to_on_domains, list):
+ self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
+
+ # Get whether or not entities should be exposed by default, or if only
+ # explicitly marked ones will be exposed
+ self.expose_by_default = conf.get(
+ CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
+
+ # Get domains that are exposed by default when expose_by_default is
+ # True
+ self.exposed_domains = conf.get(
+ CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
+
+ def entity_id_to_number(self, entity_id):
+ """Get a unique number for the entity id."""
+ if self.type == TYPE_ALEXA:
+ return entity_id
+
+ # Google Home
+ for number, ent_id in self.numbers.items():
+ if entity_id == ent_id:
+ return number
+
+ number = str(len(self.numbers) + 1)
+ self.numbers[number] = entity_id
+ return number
+
+ def number_to_entity_id(self, number):
+ """Convert unique number to entity id."""
+ if self.type == TYPE_ALEXA:
+ return number
+
+ # Google Home
+ assert isinstance(number, str)
+ return self.numbers.get(number)
+
+ def is_entity_exposed(self, entity):
+ """Determine if an entity should be exposed on the emulated bridge.
+
+ Async friendly.
+ """
+ if entity.attributes.get('view') is not None:
+ # Ignore entities that are views
+ return False
+
+ domain = entity.domain.lower()
+ explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
+
+ domain_exposed_by_default = \
+ self.expose_by_default and domain in self.exposed_domains
+
+ # Expose an entity if the entity's domain is exposed by default and
+ # the configuration doesn't explicitly exclude it from being
+ # exposed, or if the entity is explicitly exposed
+ is_default_exposed = \
+ domain_exposed_by_default and explicit_expose is not False
+
+ return is_default_exposed or explicit_expose
diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py
new file mode 100644
index 00000000000..ed06da9495b
--- /dev/null
+++ b/homeassistant/components/emulated_hue/hue_api.py
@@ -0,0 +1,275 @@
+"""Provides a Hue API to control Home Assistant."""
+import asyncio
+import logging
+
+from aiohttp import web
+
+from homeassistant import core
+from homeassistant.const import (
+ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
+ STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
+)
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
+)
+from homeassistant.components.http import HomeAssistantView
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_EMULATED_HUE = 'emulated_hue'
+ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
+
+HUE_API_STATE_ON = 'on'
+HUE_API_STATE_BRI = 'bri'
+
+
+class HueUsernameView(HomeAssistantView):
+ """Handle requests to create a username for the emulated hue bridge."""
+
+ url = '/api'
+ name = 'emulated_hue:api:create_username'
+ extra_urls = ['/api/']
+ requires_auth = False
+
+ @asyncio.coroutine
+ def post(self, request):
+ """Handle a POST request."""
+ 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',
+ HTTP_BAD_REQUEST)
+
+ return self.json([{'success': {'username': '12345678901234567890'}}])
+
+
+class HueAllLightsStateView(HomeAssistantView):
+ """Handle requests for getting and setting info about entities."""
+
+ url = '/api/{username}/lights'
+ name = 'emulated_hue:lights:state'
+ requires_auth = False
+
+ def __init__(self, config):
+ """Initialize the instance of the view."""
+ self.config = config
+
+ @core.callback
+ def get(self, request, username):
+ """Process a request to get the list of available lights."""
+ hass = request.app['hass']
+ json_response = {}
+
+ for entity in hass.states.async_all():
+ if self.config.is_entity_exposed(entity):
+ number = self.config.entity_id_to_number(entity.entity_id)
+ json_response[number] = entity_to_json(entity)
+
+ return self.json(json_response)
+
+
+class HueOneLightStateView(HomeAssistantView):
+ """Handle requests for getting and setting info about entities."""
+
+ url = '/api/{username}/lights/{entity_id}'
+ name = 'emulated_hue:light:state'
+ requires_auth = False
+
+ def __init__(self, config):
+ """Initialize the instance of the view."""
+ self.config = config
+
+ @core.callback
+ def get(self, request, username, entity_id=None):
+ """Process a request to get the state of an individual light."""
+ hass = request.app['hass']
+ entity_id = self.config.number_to_entity_id(entity_id)
+ entity = hass.states.get(entity_id)
+
+ if entity is None:
+ _LOGGER.error('Entity not found: %s', entity_id)
+ return web.Response(text="Entity not found", status=404)
+
+ if not self.config.is_entity_exposed(entity):
+ _LOGGER.error('Entity not exposed: %s', entity_id)
+ return web.Response(text="Entity not exposed", status=404)
+
+ cached_state = self.config.cached_states.get(entity_id, None)
+
+ if cached_state is None:
+ final_state = entity.state == STATE_ON
+ final_brightness = entity.attributes.get(
+ ATTR_BRIGHTNESS, 255 if final_state else 0)
+ else:
+ final_state, final_brightness = cached_state
+
+ json_response = entity_to_json(entity, final_state, final_brightness)
+
+ return self.json(json_response)
+
+
+class HueOneLightChangeView(HomeAssistantView):
+ """Handle requests for getting and setting info about entities."""
+
+ url = '/api/{username}/lights/{entity_number}/state'
+ name = 'emulated_hue:light:state'
+ requires_auth = False
+
+ def __init__(self, config):
+ """Initialize the instance of the view."""
+ self.config = config
+
+ @asyncio.coroutine
+ def put(self, request, username, entity_number):
+ """Process a request to set the state of an individual light."""
+ config = self.config
+ hass = request.app['hass']
+ entity_id = config.number_to_entity_id(entity_number)
+
+ if entity_id is None:
+ _LOGGER.error('Unknown entity number: %s', entity_number)
+ return self.json_message('Entity not found', HTTP_NOT_FOUND)
+
+ entity = hass.states.get(entity_id)
+
+ if entity is None:
+ _LOGGER.error('Entity not found: %s', entity_id)
+ return self.json_message('Entity not found', HTTP_NOT_FOUND)
+
+ if not config.is_entity_exposed(entity):
+ _LOGGER.error('Entity not exposed: %s', entity_id)
+ return web.Response(text="Entity not exposed", status=404)
+
+ try:
+ request_json = yield from request.json()
+ except ValueError:
+ _LOGGER.error('Received invalid json')
+ return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
+
+ # Parse the request into requested "on" status and brightness
+ parsed = parse_hue_api_put_light_body(request_json, entity)
+
+ if parsed is None:
+ _LOGGER.error('Unable to parse data: %s', request_json)
+ return web.Response(text="Bad request", status=400)
+
+ result, brightness = parsed
+
+ # Convert the resulting "on" status into the service we need to call
+ service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
+
+ # Construct what we need to send to the service
+ data = {ATTR_ENTITY_ID: entity_id}
+
+ # If the requested entity is a script add some variables
+ if entity.domain == "script":
+ data['variables'] = {
+ 'requested_state': STATE_ON if result else STATE_OFF
+ }
+
+ if brightness is not None:
+ data['variables']['requested_level'] = brightness
+
+ elif brightness is not None:
+ data[ATTR_BRIGHTNESS] = brightness
+
+ if entity.domain in config.off_maps_to_on_domains:
+ # Map the off command to on
+ service = SERVICE_TURN_ON
+
+ # Caching is required because things like scripts and scenes won't
+ # report as "off" to Alexa if an "off" command is received, because
+ # they'll map to "on". Thus, instead of reporting its actual
+ # status, we report what Alexa will want to see, which is the same
+ # as the actual requested command.
+ config.cached_states[entity_id] = (result, brightness)
+
+ # Perform the requested action
+ yield from hass.services.async_call(core.DOMAIN, service, data,
+ blocking=True)
+
+ json_response = \
+ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
+
+ if brightness is not None:
+ json_response.append(create_hue_success_response(
+ entity_id, HUE_API_STATE_BRI, brightness))
+
+ return self.json(json_response)
+
+
+def parse_hue_api_put_light_body(request_json, entity):
+ """Parse the body of a request to change the state of a light."""
+ if HUE_API_STATE_ON in request_json:
+ if not isinstance(request_json[HUE_API_STATE_ON], bool):
+ return None
+
+ if request_json['on']:
+ # Echo requested device be turned on
+ brightness = None
+ report_brightness = False
+ result = True
+ else:
+ # Echo requested device be turned off
+ brightness = None
+ report_brightness = False
+ result = False
+
+ if HUE_API_STATE_BRI in request_json:
+ # Make sure the entity actually supports brightness
+ entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+
+ if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
+ try:
+ # Clamp brightness from 0 to 255
+ brightness = \
+ max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
+ except ValueError:
+ return None
+
+ report_brightness = True
+ result = (brightness > 0)
+ elif entity.domain.lower() == "script":
+ # Convert 0-255 to 0-100
+ level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
+
+ brightness = round(level)
+ report_brightness = True
+ result = True
+
+ return (result, brightness) if report_brightness else (result, None)
+
+
+def entity_to_json(entity, is_on=None, brightness=None):
+ """Convert an entity to its Hue bridge JSON representation."""
+ if is_on is None:
+ is_on = entity.state == STATE_ON
+
+ if brightness is None:
+ brightness = 255 if is_on else 0
+
+ name = entity.attributes.get(
+ ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
+
+ return {
+ 'state':
+ {
+ HUE_API_STATE_ON: is_on,
+ HUE_API_STATE_BRI: brightness,
+ 'reachable': True
+ },
+ 'type': 'Dimmable light',
+ 'name': name,
+ 'modelid': 'HASS123',
+ 'uniqueid': entity.entity_id,
+ 'swversion': '123'
+ }
+
+
+def create_hue_success_response(entity_id, attr, value):
+ """Create a success response for an attribute set on a light."""
+ success_key = '/lights/{}/state/{}'.format(entity_id, attr)
+ return {'success': {success_key: value}}
diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py
new file mode 100644
index 00000000000..f81a8c1b68d
--- /dev/null
+++ b/homeassistant/components/emulated_hue/upnp.py
@@ -0,0 +1,166 @@
+"""Provides a UPNP discovery method that mimicks Hue hubs."""
+import threading
+import socket
+import logging
+import os
+import select
+
+from aiohttp import web
+
+from homeassistant import core
+from homeassistant.components.http import HomeAssistantView
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DescriptionXmlView(HomeAssistantView):
+ """Handles requests for the description.xml file."""
+
+ url = '/description.xml'
+ name = 'description:xml'
+ requires_auth = False
+
+ def __init__(self, config):
+ """Initialize the instance of the view."""
+ self.config = config
+
+ @core.callback
+ def get(self, request):
+ """Handle a GET request."""
+ xml_template = """
+
+
+1
+0
+
+http://{0}:{1}/
+
+urn:schemas-upnp-org:device:Basic:1
+HASS Bridge ({0})
+Royal Philips Electronics
+http://www.philips.com
+Philips hue Personal Wireless Lighting
+Philips hue bridge 2015
+BSB002
+http://www.meethue.com
+1234
+uuid:2f402f80-da50-11e1-9b23-001788255acc
+
+
+"""
+
+ resp_text = xml_template.format(
+ self.config.host_ip_addr, self.config.listen_port)
+
+ return web.Response(text=resp_text, content_type='text/xml')
+
+
+class UPNPResponderThread(threading.Thread):
+ """Handle responding to UPNP/SSDP discovery requests."""
+
+ _interrupted = False
+
+ def __init__(self, host_ip_addr, listen_port):
+ """Initialize the class."""
+ threading.Thread.__init__(self)
+
+ self.host_ip_addr = host_ip_addr
+ self.listen_port = listen_port
+
+ # Note that the double newline at the end of
+ # this string is required per the SSDP spec
+ resp_template = """HTTP/1.1 200 OK
+CACHE-CONTROL: max-age=60
+EXT:
+LOCATION: http://{0}:{1}/description.xml
+SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
+ST: urn:schemas-upnp-org:device:basic:1
+USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
+
+"""
+
+ self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
+ .replace("\n", "\r\n") \
+ .encode('utf-8')
+
+ # Set up a pipe for signaling to the receiver that it's time to
+ # shutdown. Essentially, we place the SSDP socket into nonblocking
+ # mode and use select() to wait for data to arrive on either the SSDP
+ # socket or the pipe. If data arrives on either one, select() returns
+ # and tells us which filenos have data ready to read.
+ #
+ # When we want to stop the responder, we write data to the pipe, which
+ # causes the select() to return and indicate that said pipe has data
+ # ready to be read, which indicates to us that the responder needs to
+ # be shutdown.
+ self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
+
+ def run(self):
+ """Run the server."""
+ # Listen for UDP port 1900 packets sent to SSDP multicast address
+ ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ ssdp_socket.setblocking(False)
+
+ # Required for receiving multicast
+ ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ ssdp_socket.setsockopt(
+ socket.SOL_IP,
+ socket.IP_MULTICAST_IF,
+ socket.inet_aton(self.host_ip_addr))
+
+ ssdp_socket.setsockopt(
+ socket.SOL_IP,
+ socket.IP_ADD_MEMBERSHIP,
+ socket.inet_aton("239.255.255.250") +
+ socket.inet_aton(self.host_ip_addr))
+
+ ssdp_socket.bind(("239.255.255.250", 1900))
+
+ while True:
+ if self._interrupted:
+ clean_socket_close(ssdp_socket)
+ return
+
+ try:
+ read, _, _ = select.select(
+ [self._interrupted_read_pipe, ssdp_socket], [],
+ [ssdp_socket])
+
+ if self._interrupted_read_pipe in read:
+ # Implies self._interrupted is True
+ clean_socket_close(ssdp_socket)
+ return
+ elif ssdp_socket in read:
+ data, addr = ssdp_socket.recvfrom(1024)
+ else:
+ continue
+ except socket.error as ex:
+ if self._interrupted:
+ clean_socket_close(ssdp_socket)
+ return
+
+ _LOGGER.error("UPNP Responder socket exception occured: %s",
+ ex.__str__)
+
+ if "M-SEARCH" in data.decode('utf-8'):
+ # SSDP M-SEARCH method received, respond to it with our info
+ resp_socket = socket.socket(
+ socket.AF_INET, socket.SOCK_DGRAM)
+
+ resp_socket.sendto(self.upnp_response, addr)
+ resp_socket.close()
+
+ def stop(self):
+ """Stop the server."""
+ # Request for server
+ self._interrupted = True
+ os.write(self._interrupted_write_pipe, bytes([0]))
+ self.join()
+
+
+def clean_socket_close(sock):
+ """Close a socket connection and logs its closure."""
+ _LOGGER.info("UPNP responder shutting down.")
+
+ sock.close()
diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py
index bf38b12237e..245e6a12cc2 100644
--- a/homeassistant/components/homematic.py
+++ b/homeassistant/components/homematic.py
@@ -544,19 +544,19 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
# keypress event
if attribute in HM_PRESS_EVENTS:
- hass.bus.fire(EVENT_KEYPRESS, {
+ hass.add_job(hass.bus.async_fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_PARAM: attribute,
ATTR_CHANNEL: channel
- })
+ }))
return
# impulse event
if attribute in HM_IMPULSE_EVENTS:
- hass.bus.fire(EVENT_KEYPRESS, {
+ hass.add_job(hass.bus.async_fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_CHANNEL: channel
- })
+ }))
return
_LOGGER.warning("Event is unknown and not forwarded to HA")
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index dc18dd2481d..11a9e755bb4 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -277,9 +277,15 @@ class HomeAssistantWSGI(object):
@asyncio.coroutine
def start(self):
"""Start the wsgi server."""
+ cors_added = set()
if self.cors is not None:
for route in list(self.app.router.routes()):
+ if hasattr(route, 'resource'):
+ route = route.resource
+ if route in cors_added:
+ continue
self.cors.add(route)
+ cors_added.add(route)
if self.ssl_certificate:
context = ssl.SSLContext(SSL_VERSION)
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index b5367486e38..1fa1a39633d 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -59,7 +59,7 @@ ATTR_SLEEP_TIME = 'sleep_time'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
- vol.Optional(CONF_HOSTS): cv.ensure_list(cv.string),
+ vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
})
SONOS_SCHEMA = vol.Schema({
diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py
index 01f7d6ab287..e19011c47b8 100644
--- a/homeassistant/components/nest.py
+++ b/homeassistant/components/nest.py
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [
'http://github.com/technicalpickles/python-nest'
- '/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip' # nest-cam branch
+ '/archive/2512973b4b390d3965da43529cd20402ad374bfa.zip' # nest-cam branch
'#python-nest==3.0.0']
DOMAIN = 'nest'
diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py
index 31201879207..6dc23a71d9b 100644
--- a/homeassistant/components/sensor/synologydsm.py
+++ b/homeassistant/components/sensor/synologydsm.py
@@ -151,15 +151,9 @@ class SynoApi():
except:
_LOGGER.error("Error setting up Synology DSM")
- def utilisation(self):
- """Return utilisation information from API."""
- if self._api is not None:
- return self._api.utilisation
-
- def storage(self):
- """Return storage information from API."""
- if self._api is not None:
- return self._api.storage
+ # Will be updated when `update` gets called.
+ self.utilisation = self._api.utilisation
+ self.storage = self._api.storage
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
@@ -219,14 +213,14 @@ class SynoNasUtilSensor(SynoNasSensor):
'memory_total_swap', 'memory_total_real']
if self.var_id in network_sensors or self.var_id in memory_sensors:
- attr = getattr(self._api.utilisation(), self.var_id)(False)
+ attr = getattr(self._api.utilisation, self.var_id)(False)
if self.var_id in network_sensors:
return round(attr / 1024.0, 1)
elif self.var_id in memory_sensors:
return round(attr / 1024.0 / 1024.0, 1)
else:
- return getattr(self._api.utilisation(), self.var_id)
+ return getattr(self._api.utilisation, self.var_id)
class SynoNasStorageSensor(SynoNasSensor):
@@ -240,7 +234,7 @@ class SynoNasStorageSensor(SynoNasSensor):
if self.monitor_device is not None:
if self.var_id in temp_sensors:
- attr = getattr(self._api.storage(),
+ attr = getattr(self._api.storage,
self.var_id)(self.monitor_device)
if self._api.temp_unit == TEMP_CELSIUS:
@@ -248,5 +242,5 @@ class SynoNasStorageSensor(SynoNasSensor):
else:
return round(attr * 1.8 + 32.0, 1)
else:
- return getattr(self._api.storage(),
+ return getattr(self._api.storage,
self.var_id)(self.monitor_device)
diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py
index 41c1d0462b3..bcc1b329fa8 100644
--- a/homeassistant/components/switch/tplink.py
+++ b/homeassistant/components/switch/tplink.py
@@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/GadgetReactor/pyHS100/archive/'
- 'fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1']
+ '1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 8e45ec4bb43..cbbcf9b6762 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 34
-PATCH_VERSION = '0'
+PATCH_VERSION = '1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
diff --git a/requirements_all.txt b/requirements_all.txt
index 56772281632..6ac20e51844 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -164,13 +164,13 @@ hikvision==0.4
# http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0
# homeassistant.components.nest
-http://github.com/technicalpickles/python-nest/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip#python-nest==3.0.0
+http://github.com/technicalpickles/python-nest/archive/2512973b4b390d3965da43529cd20402ad374bfa.zip#python-nest==3.0.0
# homeassistant.components.light.flux_led
https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9
# homeassistant.components.switch.tplink
-https://github.com/GadgetReactor/pyHS100/archive/fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1
+https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0
# homeassistant.components.switch.dlink
https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7
diff --git a/tests/components/emulated_hue/__init__.py b/tests/components/emulated_hue/__init__.py
new file mode 100644
index 00000000000..b13b95a080a
--- /dev/null
+++ b/tests/components/emulated_hue/__init__.py
@@ -0,0 +1 @@
+"""Tests for emulated_hue."""
diff --git a/tests/components/test_emulated_hue.py b/tests/components/emulated_hue/test_hue_api.py
old mode 100755
new mode 100644
similarity index 78%
rename from tests/components/test_emulated_hue.py
rename to tests/components/emulated_hue/test_hue_api.py
index 7bb8da09e47..9cee27f570f
--- a/tests/components/test_emulated_hue.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -1,429 +1,355 @@
-"""The tests for the emulated Hue component."""
-import json
-
-import unittest
-import requests
-
-from homeassistant import bootstrap, const, core
-import homeassistant.components as core_components
-from homeassistant.components import emulated_hue, http, light, script
-from homeassistant.const import STATE_ON, STATE_OFF
-from homeassistant.components.emulated_hue import (
- HUE_API_STATE_ON, HUE_API_STATE_BRI)
-from homeassistant.util.async import run_coroutine_threadsafe
-
-from tests.common import get_test_instance_port, get_test_home_assistant
-
-HTTP_SERVER_PORT = get_test_instance_port()
-BRIDGE_SERVER_PORT = get_test_instance_port()
-
-BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
-JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
-
-
-def setup_hass_instance(emulated_hue_config):
- """Set up the Home Assistant instance to test."""
- hass = get_test_home_assistant()
-
- # We need to do this to get access to homeassistant/turn_(on,off)
- run_coroutine_threadsafe(
- core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
- ).result()
-
- bootstrap.setup_component(
- hass, http.DOMAIN,
- {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
-
- bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config)
-
- return hass
-
-
-def start_hass_instance(hass):
- """Start the Home Assistant instance to test."""
- hass.start()
-
-
-class TestEmulatedHue(unittest.TestCase):
- """Test the emulated Hue component."""
-
- hass = None
-
- @classmethod
- def setUpClass(cls):
- """Setup the class."""
- cls.hass = setup_hass_instance({
- emulated_hue.DOMAIN: {
- emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT
- }})
-
- start_hass_instance(cls.hass)
-
- @classmethod
- def tearDownClass(cls):
- """Stop the class."""
- cls.hass.stop()
-
- def test_description_xml(self):
- """Test the description."""
- import xml.etree.ElementTree as ET
-
- result = requests.get(
- BRIDGE_URL_BASE.format('/description.xml'), timeout=5)
-
- self.assertEqual(result.status_code, 200)
- self.assertTrue('text/xml' in result.headers['content-type'])
-
- # Make sure the XML is parsable
- # pylint: disable=bare-except
- try:
- ET.fromstring(result.text)
- except:
- self.fail('description.xml is not valid XML!')
-
- def test_create_username(self):
- """Test the creation of an username."""
- request_json = {'devicetype': 'my_device'}
-
- result = requests.post(
- BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
- timeout=5)
-
- self.assertEqual(result.status_code, 200)
- self.assertTrue('application/json' in result.headers['content-type'])
-
- resp_json = result.json()
- success_json = resp_json[0]
-
- self.assertTrue('success' in success_json)
- self.assertTrue('username' in success_json['success'])
-
- def test_valid_username_request(self):
- """Test request with a valid username."""
- request_json = {'invalid_key': 'my_device'}
-
- result = requests.post(
- BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
- timeout=5)
-
- self.assertEqual(result.status_code, 400)
-
-
-class TestEmulatedHueExposedByDefault(unittest.TestCase):
- """Test class for emulated hue component."""
-
- @classmethod
- def setUpClass(cls):
- """Setup the class."""
- cls.hass = setup_hass_instance({
- emulated_hue.DOMAIN: {
- emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT,
- emulated_hue.CONF_EXPOSE_BY_DEFAULT: True
- }
- })
-
- bootstrap.setup_component(cls.hass, light.DOMAIN, {
- 'light': [
- {
- 'platform': 'demo',
- }
- ]
- })
-
- bootstrap.setup_component(cls.hass, script.DOMAIN, {
- 'script': {
- 'set_kitchen_light': {
- 'sequence': [
- {
- 'service_template':
- "light.turn_{{ requested_state }}",
- 'data_template': {
- 'entity_id': 'light.kitchen_lights',
- 'brightness': "{{ requested_level }}"
- }
- }
- ]
- }
- }
- })
-
- start_hass_instance(cls.hass)
-
- # Kitchen light is explicitly excluded from being exposed
- kitchen_light_entity = cls.hass.states.get('light.kitchen_lights')
- attrs = dict(kitchen_light_entity.attributes)
- attrs[emulated_hue.ATTR_EMULATED_HUE] = False
- cls.hass.states.set(
- kitchen_light_entity.entity_id, kitchen_light_entity.state,
- attributes=attrs)
-
- # Expose the script
- script_entity = cls.hass.states.get('script.set_kitchen_light')
- attrs = dict(script_entity.attributes)
- attrs[emulated_hue.ATTR_EMULATED_HUE] = True
- cls.hass.states.set(
- script_entity.entity_id, script_entity.state, attributes=attrs
- )
-
- @classmethod
- def tearDownClass(cls):
- """Stop the class."""
- cls.hass.stop()
-
- def test_discover_lights(self):
- """Test the discovery of lights."""
- result = requests.get(
- BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
-
- self.assertEqual(result.status_code, 200)
- self.assertTrue('application/json' in result.headers['content-type'])
-
- result_json = result.json()
-
- # Make sure the lights we added to the config are there
- self.assertTrue('light.ceiling_lights' in result_json)
- self.assertTrue('light.bed_light' in result_json)
- self.assertTrue('script.set_kitchen_light' in result_json)
- self.assertTrue('light.kitchen_lights' not in result_json)
-
- def test_get_light_state(self):
- """Test the getting of light state."""
- # Turn office light on and set to 127 brightness
- self.hass.services.call(
- light.DOMAIN, const.SERVICE_TURN_ON,
- {
- const.ATTR_ENTITY_ID: 'light.ceiling_lights',
- light.ATTR_BRIGHTNESS: 127
- },
- blocking=True)
-
- office_json = self.perform_get_light_state('light.ceiling_lights', 200)
-
- self.assertEqual(office_json['state'][HUE_API_STATE_ON], True)
- self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
-
- # Turn bedroom light off
- self.hass.services.call(
- light.DOMAIN, const.SERVICE_TURN_OFF,
- {
- const.ATTR_ENTITY_ID: 'light.bed_light'
- },
- blocking=True)
-
- bedroom_json = self.perform_get_light_state('light.bed_light', 200)
-
- self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False)
- self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0)
-
- # Make sure kitchen light isn't accessible
- kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights')
- kitchen_result = requests.get(
- BRIDGE_URL_BASE.format(kitchen_url), timeout=5)
-
- self.assertEqual(kitchen_result.status_code, 404)
-
- def test_put_light_state(self):
- """Test the seeting of light states."""
- self.perform_put_test_on_ceiling_lights()
-
- # Turn the bedroom light on first
- self.hass.services.call(
- light.DOMAIN, const.SERVICE_TURN_ON,
- {const.ATTR_ENTITY_ID: 'light.bed_light',
- light.ATTR_BRIGHTNESS: 153},
- blocking=True)
-
- bed_light = self.hass.states.get('light.bed_light')
- self.assertEqual(bed_light.state, STATE_ON)
- self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153)
-
- # Go through the API to turn it off
- bedroom_result = self.perform_put_light_state(
- 'light.bed_light', False)
-
- bedroom_result_json = bedroom_result.json()
-
- self.assertEqual(bedroom_result.status_code, 200)
- self.assertTrue(
- 'application/json' in bedroom_result.headers['content-type'])
-
- self.assertEqual(len(bedroom_result_json), 1)
-
- # Check to make sure the state changed
- bed_light = self.hass.states.get('light.bed_light')
- self.assertEqual(bed_light.state, STATE_OFF)
-
- # Make sure we can't change the kitchen light state
- kitchen_result = self.perform_put_light_state(
- 'light.kitchen_light', True)
- self.assertEqual(kitchen_result.status_code, 404)
-
- def test_put_light_state_script(self):
- """Test the setting of script variables."""
- # Turn the kitchen light off first
- self.hass.services.call(
- light.DOMAIN, const.SERVICE_TURN_OFF,
- {const.ATTR_ENTITY_ID: 'light.kitchen_lights'},
- blocking=True)
-
- # Emulated hue converts 0-100% to 0-255.
- level = 23
- brightness = round(level * 255 / 100)
-
- script_result = self.perform_put_light_state(
- 'script.set_kitchen_light', True, brightness)
-
- script_result_json = script_result.json()
-
- self.assertEqual(script_result.status_code, 200)
- self.assertEqual(len(script_result_json), 2)
-
- # Wait until script is complete before continuing
- self.hass.block_till_done()
-
- kitchen_light = self.hass.states.get('light.kitchen_lights')
- self.assertEqual(kitchen_light.state, 'on')
- self.assertEqual(
- kitchen_light.attributes[light.ATTR_BRIGHTNESS],
- level)
-
- # pylint: disable=invalid-name
- def test_put_with_form_urlencoded_content_type(self):
- """Test the form with urlencoded content."""
- # Needed for Alexa
- self.perform_put_test_on_ceiling_lights(
- 'application/x-www-form-urlencoded')
-
- # Make sure we fail gracefully when we can't parse the data
- data = {'key1': 'value1', 'key2': 'value2'}
- result = requests.put(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format(
- 'light.ceiling_lights')), data=data)
-
- self.assertEqual(result.status_code, 400)
-
- def test_entity_not_found(self):
- """Test for entity which are not found."""
- result = requests.get(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}'.format("not.existant_entity")),
- timeout=5)
-
- self.assertEqual(result.status_code, 404)
-
- result = requests.put(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format("non.existant_entity")),
- timeout=5)
-
- self.assertEqual(result.status_code, 404)
-
- def test_allowed_methods(self):
- """Test the allowed methods."""
- result = requests.get(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format(
- "light.ceiling_lights")))
-
- self.assertEqual(result.status_code, 405)
-
- result = requests.put(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}'.format("light.ceiling_lights")),
- data={'key1': 'value1'})
-
- self.assertEqual(result.status_code, 405)
-
- result = requests.put(
- BRIDGE_URL_BASE.format('/api/username/lights'),
- data={'key1': 'value1'})
-
- self.assertEqual(result.status_code, 405)
-
- def test_proper_put_state_request(self):
- """Test the request to set the state."""
- # Test proper on value parsing
- result = requests.put(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format(
- 'light.ceiling_lights')),
- data=json.dumps({HUE_API_STATE_ON: 1234}))
-
- self.assertEqual(result.status_code, 400)
-
- # Test proper brightness value parsing
- result = requests.put(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format(
- 'light.ceiling_lights')), data=json.dumps({
- HUE_API_STATE_ON: True,
- HUE_API_STATE_BRI: 'Hello world!'
- }))
-
- self.assertEqual(result.status_code, 400)
-
- # pylint: disable=invalid-name
- def perform_put_test_on_ceiling_lights(self,
- content_type='application/json'):
- """Test the setting of a light."""
- # Turn the office light off first
- self.hass.services.call(
- light.DOMAIN, const.SERVICE_TURN_OFF,
- {const.ATTR_ENTITY_ID: 'light.ceiling_lights'},
- blocking=True)
-
- ceiling_lights = self.hass.states.get('light.ceiling_lights')
- self.assertEqual(ceiling_lights.state, STATE_OFF)
-
- # Go through the API to turn it on
- office_result = self.perform_put_light_state(
- 'light.ceiling_lights', True, 56, content_type)
-
- office_result_json = office_result.json()
-
- self.assertEqual(office_result.status_code, 200)
- self.assertTrue(
- 'application/json' in office_result.headers['content-type'])
-
- self.assertEqual(len(office_result_json), 2)
-
- # Check to make sure the state changed
- ceiling_lights = self.hass.states.get('light.ceiling_lights')
- self.assertEqual(ceiling_lights.state, STATE_ON)
- self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56)
-
- def perform_get_light_state(self, entity_id, expected_status):
- """Test the gettting of a light state."""
- result = requests.get(
- BRIDGE_URL_BASE.format(
- '/api/username/lights/{}'.format(entity_id)), timeout=5)
-
- self.assertEqual(result.status_code, expected_status)
-
- if expected_status == 200:
- self.assertTrue(
- 'application/json' in result.headers['content-type'])
-
- return result.json()
-
- return None
-
- # pylint: disable=no-self-use
- def perform_put_light_state(self, entity_id, is_on, brightness=None,
- content_type='application/json'):
- """Test the setting of a light state."""
- url = BRIDGE_URL_BASE.format(
- '/api/username/lights/{}/state'.format(entity_id))
-
- req_headers = {'Content-Type': content_type}
-
- data = {HUE_API_STATE_ON: is_on}
-
- if brightness is not None:
- data[HUE_API_STATE_BRI] = brightness
-
- result = requests.put(
- url, data=json.dumps(data), timeout=5, headers=req_headers)
-
- return result
+"""The tests for the emulated Hue component."""
+import json
+
+import unittest
+from unittest.mock import patch
+import requests
+
+from homeassistant import bootstrap, const, core
+import homeassistant.components as core_components
+from homeassistant.components import emulated_hue, http, light, script
+from homeassistant.const import STATE_ON, STATE_OFF
+from homeassistant.components.emulated_hue.hue_api import (
+ HUE_API_STATE_ON, HUE_API_STATE_BRI)
+from homeassistant.util.async import run_coroutine_threadsafe
+
+from tests.common import get_test_instance_port, get_test_home_assistant
+
+HTTP_SERVER_PORT = get_test_instance_port()
+BRIDGE_SERVER_PORT = get_test_instance_port()
+
+BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
+JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
+
+
+class TestEmulatedHueExposedByDefault(unittest.TestCase):
+ """Test class for emulated hue component."""
+
+ @classmethod
+ def setUpClass(cls):
+ """Setup the class."""
+ cls.hass = hass = get_test_home_assistant()
+
+ # We need to do this to get access to homeassistant/turn_(on,off)
+ run_coroutine_threadsafe(
+ core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
+ ).result()
+
+ bootstrap.setup_component(
+ hass, http.DOMAIN,
+ {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
+
+ with patch('homeassistant.components'
+ '.emulated_hue.UPNPResponderThread'):
+ bootstrap.setup_component(hass, emulated_hue.DOMAIN, {
+ emulated_hue.DOMAIN: {
+ emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT,
+ emulated_hue.CONF_EXPOSE_BY_DEFAULT: True
+ }
+ })
+
+ bootstrap.setup_component(cls.hass, light.DOMAIN, {
+ 'light': [
+ {
+ 'platform': 'demo',
+ }
+ ]
+ })
+
+ bootstrap.setup_component(cls.hass, script.DOMAIN, {
+ 'script': {
+ 'set_kitchen_light': {
+ 'sequence': [
+ {
+ 'service_template':
+ "light.turn_{{ requested_state }}",
+ 'data_template': {
+ 'entity_id': 'light.kitchen_lights',
+ 'brightness': "{{ requested_level }}"
+ }
+ }
+ ]
+ }
+ }
+ })
+
+ cls.hass.start()
+
+ # Kitchen light is explicitly excluded from being exposed
+ kitchen_light_entity = cls.hass.states.get('light.kitchen_lights')
+ attrs = dict(kitchen_light_entity.attributes)
+ attrs[emulated_hue.ATTR_EMULATED_HUE] = False
+ cls.hass.states.set(
+ kitchen_light_entity.entity_id, kitchen_light_entity.state,
+ attributes=attrs)
+
+ # Expose the script
+ script_entity = cls.hass.states.get('script.set_kitchen_light')
+ attrs = dict(script_entity.attributes)
+ attrs[emulated_hue.ATTR_EMULATED_HUE] = True
+ cls.hass.states.set(
+ script_entity.entity_id, script_entity.state, attributes=attrs
+ )
+
+ @classmethod
+ def tearDownClass(cls):
+ """Stop the class."""
+ cls.hass.stop()
+
+ def test_discover_lights(self):
+ """Test the discovery of lights."""
+ result = requests.get(
+ BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
+
+ self.assertEqual(result.status_code, 200)
+ self.assertTrue('application/json' in result.headers['content-type'])
+
+ result_json = result.json()
+
+ # Make sure the lights we added to the config are there
+ self.assertTrue('light.ceiling_lights' in result_json)
+ self.assertTrue('light.bed_light' in result_json)
+ self.assertTrue('script.set_kitchen_light' in result_json)
+ self.assertTrue('light.kitchen_lights' not in result_json)
+
+ def test_get_light_state(self):
+ """Test the getting of light state."""
+ # Turn office light on and set to 127 brightness
+ self.hass.services.call(
+ light.DOMAIN, const.SERVICE_TURN_ON,
+ {
+ const.ATTR_ENTITY_ID: 'light.ceiling_lights',
+ light.ATTR_BRIGHTNESS: 127
+ },
+ blocking=True)
+
+ office_json = self.perform_get_light_state('light.ceiling_lights', 200)
+
+ self.assertEqual(office_json['state'][HUE_API_STATE_ON], True)
+ self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
+
+ # Turn bedroom light off
+ self.hass.services.call(
+ light.DOMAIN, const.SERVICE_TURN_OFF,
+ {
+ const.ATTR_ENTITY_ID: 'light.bed_light'
+ },
+ blocking=True)
+
+ bedroom_json = self.perform_get_light_state('light.bed_light', 200)
+
+ self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False)
+ self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0)
+
+ # Make sure kitchen light isn't accessible
+ kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights')
+ kitchen_result = requests.get(
+ BRIDGE_URL_BASE.format(kitchen_url), timeout=5)
+
+ self.assertEqual(kitchen_result.status_code, 404)
+
+ def test_put_light_state(self):
+ """Test the seeting of light states."""
+ self.perform_put_test_on_ceiling_lights()
+
+ # Turn the bedroom light on first
+ self.hass.services.call(
+ light.DOMAIN, const.SERVICE_TURN_ON,
+ {const.ATTR_ENTITY_ID: 'light.bed_light',
+ light.ATTR_BRIGHTNESS: 153},
+ blocking=True)
+
+ bed_light = self.hass.states.get('light.bed_light')
+ self.assertEqual(bed_light.state, STATE_ON)
+ self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153)
+
+ # Go through the API to turn it off
+ bedroom_result = self.perform_put_light_state(
+ 'light.bed_light', False)
+
+ bedroom_result_json = bedroom_result.json()
+
+ self.assertEqual(bedroom_result.status_code, 200)
+ self.assertTrue(
+ 'application/json' in bedroom_result.headers['content-type'])
+
+ self.assertEqual(len(bedroom_result_json), 1)
+
+ # Check to make sure the state changed
+ bed_light = self.hass.states.get('light.bed_light')
+ self.assertEqual(bed_light.state, STATE_OFF)
+
+ # Make sure we can't change the kitchen light state
+ kitchen_result = self.perform_put_light_state(
+ 'light.kitchen_light', True)
+ self.assertEqual(kitchen_result.status_code, 404)
+
+ def test_put_light_state_script(self):
+ """Test the setting of script variables."""
+ # Turn the kitchen light off first
+ self.hass.services.call(
+ light.DOMAIN, const.SERVICE_TURN_OFF,
+ {const.ATTR_ENTITY_ID: 'light.kitchen_lights'},
+ blocking=True)
+
+ # Emulated hue converts 0-100% to 0-255.
+ level = 23
+ brightness = round(level * 255 / 100)
+
+ script_result = self.perform_put_light_state(
+ 'script.set_kitchen_light', True, brightness)
+
+ script_result_json = script_result.json()
+
+ self.assertEqual(script_result.status_code, 200)
+ self.assertEqual(len(script_result_json), 2)
+
+ # Wait until script is complete before continuing
+ self.hass.block_till_done()
+
+ kitchen_light = self.hass.states.get('light.kitchen_lights')
+ self.assertEqual(kitchen_light.state, 'on')
+ self.assertEqual(
+ kitchen_light.attributes[light.ATTR_BRIGHTNESS],
+ level)
+
+ # pylint: disable=invalid-name
+ def test_put_with_form_urlencoded_content_type(self):
+ """Test the form with urlencoded content."""
+ # Needed for Alexa
+ self.perform_put_test_on_ceiling_lights(
+ 'application/x-www-form-urlencoded')
+
+ # Make sure we fail gracefully when we can't parse the data
+ data = {'key1': 'value1', 'key2': 'value2'}
+ result = requests.put(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format(
+ 'light.ceiling_lights')), data=data)
+
+ self.assertEqual(result.status_code, 400)
+
+ def test_entity_not_found(self):
+ """Test for entity which are not found."""
+ result = requests.get(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}'.format("not.existant_entity")),
+ timeout=5)
+
+ self.assertEqual(result.status_code, 404)
+
+ result = requests.put(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format("non.existant_entity")),
+ timeout=5)
+
+ self.assertEqual(result.status_code, 404)
+
+ def test_allowed_methods(self):
+ """Test the allowed methods."""
+ result = requests.get(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format(
+ "light.ceiling_lights")))
+
+ self.assertEqual(result.status_code, 405)
+
+ result = requests.put(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}'.format("light.ceiling_lights")),
+ data={'key1': 'value1'})
+
+ self.assertEqual(result.status_code, 405)
+
+ result = requests.put(
+ BRIDGE_URL_BASE.format('/api/username/lights'),
+ data={'key1': 'value1'})
+
+ self.assertEqual(result.status_code, 405)
+
+ def test_proper_put_state_request(self):
+ """Test the request to set the state."""
+ # Test proper on value parsing
+ result = requests.put(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format(
+ 'light.ceiling_lights')),
+ data=json.dumps({HUE_API_STATE_ON: 1234}))
+
+ self.assertEqual(result.status_code, 400)
+
+ # Test proper brightness value parsing
+ result = requests.put(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format(
+ 'light.ceiling_lights')), data=json.dumps({
+ HUE_API_STATE_ON: True,
+ HUE_API_STATE_BRI: 'Hello world!'
+ }))
+
+ self.assertEqual(result.status_code, 400)
+
+ # pylint: disable=invalid-name
+ def perform_put_test_on_ceiling_lights(self,
+ content_type='application/json'):
+ """Test the setting of a light."""
+ # Turn the office light off first
+ self.hass.services.call(
+ light.DOMAIN, const.SERVICE_TURN_OFF,
+ {const.ATTR_ENTITY_ID: 'light.ceiling_lights'},
+ blocking=True)
+
+ ceiling_lights = self.hass.states.get('light.ceiling_lights')
+ self.assertEqual(ceiling_lights.state, STATE_OFF)
+
+ # Go through the API to turn it on
+ office_result = self.perform_put_light_state(
+ 'light.ceiling_lights', True, 56, content_type)
+
+ office_result_json = office_result.json()
+
+ self.assertEqual(office_result.status_code, 200)
+ self.assertTrue(
+ 'application/json' in office_result.headers['content-type'])
+
+ self.assertEqual(len(office_result_json), 2)
+
+ # Check to make sure the state changed
+ ceiling_lights = self.hass.states.get('light.ceiling_lights')
+ self.assertEqual(ceiling_lights.state, STATE_ON)
+ self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56)
+
+ def perform_get_light_state(self, entity_id, expected_status):
+ """Test the gettting of a light state."""
+ result = requests.get(
+ BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}'.format(entity_id)), timeout=5)
+
+ self.assertEqual(result.status_code, expected_status)
+
+ if expected_status == 200:
+ self.assertTrue(
+ 'application/json' in result.headers['content-type'])
+
+ return result.json()
+
+ return None
+
+ # pylint: disable=no-self-use
+ def perform_put_light_state(self, entity_id, is_on, brightness=None,
+ content_type='application/json'):
+ """Test the setting of a light state."""
+ url = BRIDGE_URL_BASE.format(
+ '/api/username/lights/{}/state'.format(entity_id))
+
+ req_headers = {'Content-Type': content_type}
+
+ data = {HUE_API_STATE_ON: is_on}
+
+ if brightness is not None:
+ data[HUE_API_STATE_BRI] = brightness
+
+ result = requests.put(
+ url, data=json.dumps(data), timeout=5, headers=req_headers)
+
+ return result
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
new file mode 100755
index 00000000000..ec3cc0a11cb
--- /dev/null
+++ b/tests/components/emulated_hue/test_init.py
@@ -0,0 +1,55 @@
+from unittest.mock import patch
+
+from homeassistant.components.emulated_hue import Config, _LOGGER
+
+
+def test_config_google_home_entity_id_to_number():
+ """Test config adheres to the type."""
+ conf = Config({
+ 'type': 'google_home'
+ })
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '2'
+
+ entity_id = conf.number_to_entity_id('1')
+ assert entity_id == 'light.test'
+
+
+def test_config_alexa_entity_id_to_number():
+ """Test config adheres to the type."""
+ conf = Config({
+ 'type': 'alexa'
+ })
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == 'light.test'
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == 'light.test'
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == 'light.test2'
+
+ entity_id = conf.number_to_entity_id('light.test')
+ assert entity_id == 'light.test'
+
+
+def test_warning_config_google_home_listen_port():
+ """Test we warn when non-default port is used for Google Home."""
+ with patch.object(_LOGGER, 'warning') as mock_warn:
+ Config({
+ 'type': 'google_home',
+ 'host_ip': '123.123.123.123',
+ 'listen_port': 8300
+ })
+
+ assert mock_warn.called
+ assert mock_warn.mock_calls[0][1][0] == \
+ "When targetting Google Home, listening port has to be port 80"
diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py
new file mode 100644
index 00000000000..03b9e993a9b
--- /dev/null
+++ b/tests/components/emulated_hue/test_upnp.py
@@ -0,0 +1,120 @@
+"""The tests for the emulated Hue component."""
+import json
+
+import unittest
+from unittest.mock import patch
+import requests
+
+from homeassistant import bootstrap, const, core
+import homeassistant.components as core_components
+from homeassistant.components import emulated_hue, http
+from homeassistant.util.async import run_coroutine_threadsafe
+
+from tests.common import get_test_instance_port, get_test_home_assistant
+
+HTTP_SERVER_PORT = get_test_instance_port()
+BRIDGE_SERVER_PORT = get_test_instance_port()
+
+BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
+JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
+
+
+def setup_hass_instance(emulated_hue_config):
+ """Set up the Home Assistant instance to test."""
+ hass = get_test_home_assistant()
+
+ # We need to do this to get access to homeassistant/turn_(on,off)
+ run_coroutine_threadsafe(
+ core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
+ ).result()
+
+ bootstrap.setup_component(
+ hass, http.DOMAIN,
+ {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
+
+ bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config)
+
+ return hass
+
+
+def start_hass_instance(hass):
+ """Start the Home Assistant instance to test."""
+ hass.start()
+
+
+class TestEmulatedHue(unittest.TestCase):
+ """Test the emulated Hue component."""
+
+ hass = None
+
+ @classmethod
+ def setUpClass(cls):
+ """Setup the class."""
+ cls.hass = hass = get_test_home_assistant()
+
+ # We need to do this to get access to homeassistant/turn_(on,off)
+ run_coroutine_threadsafe(
+ core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
+ ).result()
+
+ bootstrap.setup_component(
+ hass, http.DOMAIN,
+ {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
+
+ with patch('homeassistant.components'
+ '.emulated_hue.UPNPResponderThread'):
+ bootstrap.setup_component(hass, emulated_hue.DOMAIN, {
+ emulated_hue.DOMAIN: {
+ emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT
+ }})
+
+ cls.hass.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ """Stop the class."""
+ cls.hass.stop()
+
+ def test_description_xml(self):
+ """Test the description."""
+ import xml.etree.ElementTree as ET
+
+ result = requests.get(
+ BRIDGE_URL_BASE.format('/description.xml'), timeout=5)
+
+ self.assertEqual(result.status_code, 200)
+ self.assertTrue('text/xml' in result.headers['content-type'])
+
+ # Make sure the XML is parsable
+ # pylint: disable=bare-except
+ try:
+ ET.fromstring(result.text)
+ except:
+ self.fail('description.xml is not valid XML!')
+
+ def test_create_username(self):
+ """Test the creation of an username."""
+ request_json = {'devicetype': 'my_device'}
+
+ result = requests.post(
+ BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
+ timeout=5)
+
+ self.assertEqual(result.status_code, 200)
+ self.assertTrue('application/json' in result.headers['content-type'])
+
+ resp_json = result.json()
+ success_json = resp_json[0]
+
+ self.assertTrue('success' in success_json)
+ self.assertTrue('username' in success_json['success'])
+
+ def test_valid_username_request(self):
+ """Test request with a valid username."""
+ request_json = {'invalid_key': 'my_device'}
+
+ result = requests.post(
+ BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
+ timeout=5)
+
+ self.assertEqual(result.status_code, 400)
diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py
index cd0d4fe1ffa..f50e1fb9dbf 100644
--- a/tests/components/http/test_init.py
+++ b/tests/components/http/test_init.py
@@ -44,6 +44,10 @@ def setUpModule():
bootstrap.setup_component(hass, 'api')
+ # Registering static path as it caused CORS to blow up
+ hass.http.register_static_path(
+ '/custom_components', hass.config.path('custom_components'))
+
hass.start()
@@ -53,11 +57,12 @@ def tearDownModule():
hass.stop()
-class TestHttp:
+class TestCors:
"""Test HTTP component."""
def test_cors_allowed_with_password_in_url(self):
"""Test cross origin resource sharing with password in url."""
+
req = requests.get(_url(const.URL_API),
params={'api_password': API_PASSWORD},
headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL})