diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 32fb4af071c..57a4f18825a 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -6,12 +6,16 @@ from aiohttp import web from homeassistant import core from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, - STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET, + 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.media_player import ( + ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS, + SUPPORT_VOLUME_SET, +) from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -65,8 +69,11 @@ class HueAllLightsStateView(HomeAssistantView): for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): + state, brightness = get_entity_state(self.config, entity) + number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(entity) + json_response[number] = entity_to_json( + entity, state, brightness) return self.json(json_response) @@ -97,16 +104,9 @@ class HueOneLightStateView(HomeAssistantView): _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) + state, brightness = get_entity_state(self.config, entity) - 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) + json_response = entity_to_json(entity, state, brightness) return self.json(json_response) @@ -158,14 +158,24 @@ class HueOneLightChangeView(HomeAssistantView): result, brightness = parsed + # Choose general HA domain + domain = core.DOMAIN + # 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} + # Make sure the entity actually supports brightness + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: + if brightness is not None: + data[ATTR_BRIGHTNESS] = brightness + # If the requested entity is a script add some variables - if entity.domain == "script": + elif entity.domain == "script": data['variables'] = { 'requested_state': STATE_ON if result else STATE_OFF } @@ -173,8 +183,16 @@ class HueOneLightChangeView(HomeAssistantView): if brightness is not None: data['variables']['requested_level'] = brightness - elif brightness is not None: - data[ATTR_BRIGHTNESS] = brightness + # If the requested entity is a media player, convert to volume + elif entity.domain == "media_player": + media_commands = entity.attributes.get( + ATTR_SUPPORTED_MEDIA_COMMANDS, 0) + if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET: + if brightness is not None: + domain = entity.domain + service = SERVICE_VOLUME_SET + # Convert 0-100 to 0.0-1.0 + data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0 if entity.domain in config.off_maps_to_on_domains: # Map the off command to on @@ -187,9 +205,14 @@ class HueOneLightChangeView(HomeAssistantView): # 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) + # Separate call to turn on needed + if domain != core.DOMAIN: + hass.async_add_job(hass.services.async_call( + core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, + blocking=True)) + + hass.async_add_job(hass.services.async_call( + domain, service, data, blocking=True)) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] @@ -219,23 +242,23 @@ def parse_hue_api_put_light_body(request_json, entity): result = False if HUE_API_STATE_BRI in request_json: + try: + # Clamp brightness from 0 to 255 + brightness = \ + max(0, min(int(request_json[HUE_API_STATE_BRI]), 255)) + except ValueError: + return None + # 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 + elif entity.domain == "script" or entity.domain == "media_player": + # Convert 0-255 to 0-100 + level = brightness / 255 * 100 brightness = round(level) report_brightness = True result = True @@ -243,14 +266,34 @@ def parse_hue_api_put_light_body(request_json, entity): return (result, brightness) if report_brightness else (result, None) +def get_entity_state(config, entity): + """Retrieve and convert state and brightness values for an entity.""" + cached_state = config.cached_states.get(entity.entity_id, None) + + if cached_state is None: + final_state = entity.state != STATE_OFF + final_brightness = entity.attributes.get( + ATTR_BRIGHTNESS, 255 if final_state else 0) + + # Make sure the entity actually supports brightness + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: + pass + + elif entity.domain == "media_player": + level = entity.attributes.get( + ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0) + # Convert 0.0-1.0 to 0-255 + final_brightness = round(min(1.0, level) * 255) + else: + final_state, final_brightness = cached_state + + return (final_state, final_brightness) + + 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.name) return { diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 9cee27f570f..4aab6401939 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -7,7 +7,9 @@ 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.components import ( + emulated_hue, http, light, script, media_player +) from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_ON, HUE_API_STATE_BRI) @@ -73,6 +75,14 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): } }) + bootstrap.setup_component(cls.hass, media_player.DOMAIN, { + 'media_player': [ + { + 'platform': 'demo', + } + ] + }) + cls.hass.start() # Kitchen light is explicitly excluded from being exposed @@ -111,6 +121,10 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): 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) + self.assertTrue('media_player.living_room' in result_json) + self.assertTrue('media_player.bedroom' in result_json) + self.assertTrue('media_player.walkman' in result_json) + self.assertTrue('media_player.lounge_room' in result_json) def test_get_light_state(self): """Test the getting of light state.""" @@ -128,6 +142,21 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): self.assertEqual(office_json['state'][HUE_API_STATE_ON], True) self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127) + # Check all lights view + 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() + + self.assertTrue('light.ceiling_lights' in result_json) + self.assertEqual( + result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI], + 127, + ) + # Turn bedroom light off self.hass.services.call( light.DOMAIN, const.SERVICE_TURN_OFF, @@ -204,15 +233,38 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): 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) + def test_put_light_state_media_player(self): + """Test turning on media player and setting volume.""" + # Turn the music player off first + self.hass.services.call( + media_player.DOMAIN, const.SERVICE_TURN_OFF, + {const.ATTR_ENTITY_ID: 'media_player.walkman'}, + blocking=True) + + # Emulated hue converts 0.0-1.0 to 0-255. + level = 0.25 + brightness = round(level * 255) + + mp_result = self.perform_put_light_state( + 'media_player.walkman', True, brightness) + + mp_result_json = mp_result.json() + + self.assertEqual(mp_result.status_code, 200) + self.assertEqual(len(mp_result_json), 2) + + walkman = self.hass.states.get('media_player.walkman') + self.assertEqual(walkman.state, 'playing') + self.assertEqual( + walkman.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL], + level) + # pylint: disable=invalid-name def test_put_with_form_urlencoded_content_type(self): """Test the form with urlencoded content.""" @@ -352,4 +404,7 @@ class TestEmulatedHueExposedByDefault(unittest.TestCase): result = requests.put( url, data=json.dumps(data), timeout=5, headers=req_headers) + # Wait until state change is complete before continuing + self.hass.block_till_done() + return result