"""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