From 3f2b6bfaa4acc333a5a3caaea3955ab564f49ab2 Mon Sep 17 00:00:00 2001 From: NobleKangaroo <34781835+NobleKangaroo@users.noreply.github.com> Date: Mon, 2 Dec 2019 00:00:22 -0500 Subject: [PATCH] Overhaul Emulated Hue (#28317) * Emulated Hue Overhaul * Fix erroneous merge * Remove unused code * Modernize string format --- .../components/emulated_hue/hue_api.py | 397 ++++++++++-------- tests/components/emulated_hue/test_hue_api.py | 42 +- 2 files changed, 254 insertions(+), 185 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 51ce3e2e42a..e7f15e7fc53 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,7 +1,6 @@ """Support for a Hue API to control Home Assistant.""" import logging - -from aiohttp import web +import hashlib from homeassistant import core from homeassistant.components import ( @@ -36,8 +35,10 @@ from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -48,6 +49,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, HTTP_BAD_REQUEST, + HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -62,18 +64,30 @@ from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) +STATE_BRIGHTNESS = "bri" +STATE_COLORMODE = "colormode" +STATE_HUE = "hue" +STATE_SATURATION = "sat" +STATE_COLOR_TEMP = "ct" + +# Hue API states, defined separately in case they change HUE_API_STATE_ON = "on" HUE_API_STATE_BRI = "bri" +HUE_API_STATE_COLORMODE = "colormode" HUE_API_STATE_HUE = "hue" HUE_API_STATE_SAT = "sat" +HUE_API_STATE_CT = "ct" +HUE_API_STATE_EFFECT = "effect" -HUE_API_STATE_HUE_MAX = 65535.0 -HUE_API_STATE_SAT_MAX = 254.0 -HUE_API_STATE_BRI_MAX = 255.0 - -STATE_BRIGHTNESS = HUE_API_STATE_BRI -STATE_HUE = HUE_API_STATE_HUE -STATE_SATURATION = HUE_API_STATE_SAT +# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/ +HUE_API_STATE_BRI_MIN = 1 # Brightness +HUE_API_STATE_BRI_MAX = 254 +HUE_API_STATE_HUE_MIN = 0 # Hue +HUE_API_STATE_HUE_MAX = 65535 +HUE_API_STATE_SAT_MIN = 0 # Saturation +HUE_API_STATE_SAT_MAX = 254 +HUE_API_STATE_CT_MIN = 153 # Color temp +HUE_API_STATE_CT_MAX = 500 class HueUsernameView(HomeAssistantView): @@ -86,6 +100,9 @@ class HueUsernameView(HomeAssistantView): async def post(self, request): """Handle a POST request.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) + try: data = await request.json() except ValueError: @@ -94,14 +111,11 @@ class HueUsernameView(HomeAssistantView): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) class HueAllGroupsStateView(HomeAssistantView): - """Group handler.""" + """Handle requests for getting info about entity groups.""" url = "/api/{username}/groups" name = "emulated_hue:all_groups:state" @@ -115,7 +129,7 @@ class HueAllGroupsStateView(HomeAssistantView): def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({}) @@ -135,7 +149,7 @@ class HueGroupView(HomeAssistantView): def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json( [ @@ -151,7 +165,7 @@ class HueGroupView(HomeAssistantView): class HueAllLightsStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about all entities.""" url = "/api/{username}/lights" name = "emulated_hue:lights:state" @@ -165,23 +179,21 @@ class HueAllLightsStateView(HomeAssistantView): def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): - state = get_entity_state(self.config, entity) - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity, state) + json_response[number] = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about a single entity.""" url = "/api/{username}/lights/{entity_id}" name = "emulated_hue:light:state" @@ -195,7 +207,7 @@ class HueOneLightStateView(HomeAssistantView): def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) @@ -205,27 +217,25 @@ class HueOneLightStateView(HomeAssistantView): "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) 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) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) - state = get_entity_state(self.config, entity) - - json_response = entity_to_json(self.config, entity, state) + json_response = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightChangeView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for setting info about entities.""" url = "/api/{username}/lights/{entity_number}/state" name = "emulated_hue:light:state" @@ -238,7 +248,7 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config hass = request.app["hass"] @@ -256,7 +266,7 @@ class HueOneLightChangeView(HomeAssistantView): if not config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) try: request_json = await request.json() @@ -264,12 +274,60 @@ class HueOneLightChangeView(HomeAssistantView): _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) + # Get the entity's supported features + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if parsed is None: - _LOGGER.error("Unable to parse data: %s", request_json) - return web.Response(text="Bad request", status=400) + # Parse the request + parsed = { + STATE_ON: False, + STATE_BRIGHTNESS: None, + STATE_HUE: None, + STATE_SATURATION: None, + STATE_COLOR_TEMP: None, + } + + if HUE_API_STATE_ON in request_json: + if not isinstance(request_json[HUE_API_STATE_ON], bool): + _LOGGER.error("Unable to parse data: %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + parsed[STATE_ON] = request_json[HUE_API_STATE_ON] + else: + parsed[STATE_ON] = entity.state != STATE_OFF + + for (key, attr) in ( + (HUE_API_STATE_BRI, STATE_BRIGHTNESS), + (HUE_API_STATE_HUE, STATE_HUE), + (HUE_API_STATE_SAT, STATE_SATURATION), + (HUE_API_STATE_CT, STATE_COLOR_TEMP), + ): + if key in request_json: + try: + parsed[attr] = int(request_json[key]) + except ValueError: + _LOGGER.error("Unable to parse data (2): %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + + if HUE_API_STATE_BRI in request_json: + if entity.domain == light.DOMAIN: + parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 + if not entity_features & SUPPORT_BRIGHTNESS: + parsed[STATE_BRIGHTNESS] = None + + elif entity.domain == scene.DOMAIN: + parsed[STATE_BRIGHTNESS] = None + parsed[STATE_ON] = True + + elif entity.domain in [ + script.DOMAIN, + media_player.DOMAIN, + fan.DOMAIN, + cover.DOMAIN, + climate.DOMAIN, + ]: + # Convert 0-255 to 0-100 + level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 + parsed[STATE_BRIGHTNESS] = round(level) + parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN @@ -283,29 +341,37 @@ class HueOneLightChangeView(HomeAssistantView): # 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 the requested entity is a light, set the brightness, hue, + # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if entity_features & SUPPORT_BRIGHTNESS: if parsed[STATE_BRIGHTNESS] is not None: data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS] + if entity_features & SUPPORT_COLOR: - if parsed[STATE_HUE] is not None: - if parsed[STATE_SATURATION]: + if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): + if parsed[STATE_HUE] is not None: + hue = parsed[STATE_HUE] + else: + hue = 0 + + if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 - hue = parsed[STATE_HUE] # Convert hs values to hass hs values - sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) + sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) - # If the requested entity is a script add some variables + if entity_features & SUPPORT_COLOR_TEMP: + if parsed[STATE_COLOR_TEMP] is not None: + data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + + # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF @@ -366,8 +432,8 @@ class HueOneLightChangeView(HomeAssistantView): elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH + # Map the off command to on 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 @@ -393,141 +459,65 @@ class HueOneLightChangeView(HomeAssistantView): hass.services.async_call(domain, service, data, blocking=True) ) + # Create success responses for all received keys json_response = [ create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) ] - if parsed[STATE_BRIGHTNESS] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS] + for (key, val) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI), + (STATE_HUE, HUE_API_STATE_HUE), + (STATE_SATURATION, HUE_API_STATE_SAT), + (STATE_COLOR_TEMP, HUE_API_STATE_CT), + ): + if parsed[key] is not None: + json_response.append( + create_hue_success_response(entity_id, val, parsed[key]) ) - ) - if parsed[STATE_HUE] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE] - ) - ) - if parsed[STATE_SATURATION] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION] - ) - ) 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.""" - data = { - STATE_BRIGHTNESS: None, - STATE_HUE: None, - STATE_ON: False, - STATE_SATURATION: None, - } - - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if HUE_API_STATE_ON in request_json: - if not isinstance(request_json[HUE_API_STATE_ON], bool): - return None - - if request_json[HUE_API_STATE_ON]: - # Echo requested device be turned on - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - else: - # Echo requested device be turned off - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = False - - if HUE_API_STATE_HUE in request_json: - try: - # Clamp brightness from 0 to 65535 - data[STATE_HUE] = max( - 0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_SAT in request_json: - try: - # Clamp saturation from 0 to 254 - data[STATE_SATURATION] = max( - 0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_BRI in request_json: - try: - # Clamp brightness from 0 to 255 - data[STATE_BRIGHTNESS] = max( - 0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX) - ) - except ValueError: - return None - - if entity.domain == light.DOMAIN: - data[STATE_ON] = data[STATE_BRIGHTNESS] > 0 - if not entity_features & SUPPORT_BRIGHTNESS: - data[STATE_BRIGHTNESS] = None - - elif entity.domain == scene.DOMAIN: - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - - elif entity.domain in [ - script.DOMAIN, - media_player.DOMAIN, - fan.DOMAIN, - cover.DOMAIN, - climate.DOMAIN, - ]: - # Convert 0-255 to 0-100 - level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 - data[STATE_BRIGHTNESS] = round(level) - data[STATE_ON] = True - - return data - - 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) data = { + STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, - STATE_ON: False, STATE_SATURATION: None, + STATE_COLOR_TEMP: None, } if cached_state is None: data[STATE_ON] = entity.state != STATE_OFF + if data[STATE_ON]: data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0) hue_sat = entity.attributes.get(ATTR_HS_COLOR, None) if hue_sat is not None: hue = hue_sat[0] sat = hue_sat[1] - # convert hass hs values back to hue hs values + # Convert hass hs values back to hue hs values data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) + else: + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) + else: data[STATE_BRIGHTNESS] = 0 data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure the entity actually supports brightness + # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: pass - elif entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-255 @@ -537,7 +527,7 @@ def get_entity_state(config, entity): ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 ) # Convert 0.0-1.0 to 0-255 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(min(1.0, level) * 255) elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 @@ -550,12 +540,13 @@ def get_entity_state(config, entity): data[STATE_BRIGHTNESS] = 255 elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(level / 100 * 255) else: data = cached_state # Make sure brightness is valid if data[STATE_BRIGHTNESS] is None: data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0 + # Make sure hue/saturation are valid if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): data[STATE_HUE] = 0 @@ -566,39 +557,117 @@ def get_entity_state(config, entity): data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + # Clamp brightness, hue, saturation, and color temp to valid values + for (key, v_min, v_max) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), + (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), + (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX), + (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX), + ): + if data[key] is not None: + data[key] = max(v_min, min(data[key], v_max)) + return data -def entity_to_json(config, entity, state): +def entity_to_json(config, entity): """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: - return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": entity.state != STATE_UNAVAILABLE, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } - return { + unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() + unique_id = "00:{}:{}:{}:{}:{}:{}:{}-{}".format( + unique_id[0:2], + unique_id[2:4], + unique_id[4:6], + unique_id[6:8], + unique_id[8:10], + unique_id[10:12], + unique_id[12:14], + unique_id[14:16], + ) + + state = get_entity_state(config, entity) + + retval = { "state": { HUE_API_STATE_ON: state[STATE_ON], "reachable": entity.state != STATE_UNAVAILABLE, + "mode": "homeautomation", }, - "type": "On/off light", "name": config.get_entity_name(entity), - "modelid": "HASS321", - "uniqueid": entity.entity_id, + "uniqueid": unique_id, + "manufacturername": "Home Assistant", "swversion": "123", } + if ( + (entity_features & SUPPORT_BRIGHTNESS) + and (entity_features & SUPPORT_COLOR) + and (entity_features & SUPPORT_COLOR_TEMP) + ): + # Extended Color light (ZigBee Device ID: 0x0210) + # Same as Color light, but which supports additional setting of color temperature + retval["type"] = "Extended color light" + retval["modelid"] = "HASS231" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_EFFECT: "none", + } + ) + if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: + retval["state"][HUE_API_STATE_COLORMODE] = "hs" + else: + retval["state"][HUE_API_STATE_COLORMODE] = "ct" + elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + # Color light (ZigBee Device ID: 0x0200) + # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) + retval["type"] = "Color light" + retval["modelid"] = "HASS213" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_COLORMODE: "hs", + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_EFFECT: "none", + } + ) + elif (entity_features & SUPPORT_BRIGHTNESS) and ( + entity_features & SUPPORT_COLOR_TEMP + ): + # Color temperature light (ZigBee Device ID: 0x0220) + # Supports groups, scenes, on/off, dimming, and setting of a color temperature + retval["type"] = "Color temperature light" + retval["modelid"] = "HASS312" + retval["state"].update( + {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} + ) + elif ( + entity_features + & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ) + ) or entity.domain == script.DOMAIN: + # Dimmable light (ZigBee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + else: + # On/off light (ZigBee Device ID: 0x0000) + # Supports groups, scenes and on/off control + retval["type"] = "On/off light" + retval["modelid"] = "HASS321" + + return retval + def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 9629ae6cf69..4f0d70d0046 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -205,20 +205,20 @@ def test_discover_lights(hue_client): devices = set(val["uniqueid"] for val in result_json.values()) # Make sure the lights we added to the config are there - assert "light.ceiling_lights" in devices - assert "light.bed_light" not in devices - assert "script.set_kitchen_light" in devices - assert "light.kitchen_lights" not in devices - assert "media_player.living_room" in devices - assert "media_player.bedroom" in devices - assert "media_player.walkman" in devices - assert "media_player.lounge_room" in devices - assert "fan.living_room_fan" in devices - assert "fan.ceiling_fan" not in devices - assert "cover.living_room_window" in devices - assert "climate.hvac" in devices - assert "climate.heatpump" in devices - assert "climate.ecobee" not in devices + assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + assert "00:b6:14:77:34:b7:bb:06-e8" not in devices # light.bed_light + assert "00:95:b7:51:16:58:6c:c0-c5" in devices # script.set_kitchen_light + assert "00:64:7b:e4:96:c3:fe:90-c3" not in devices # light.kitchen_lights + assert "00:7e:8a:42:35:66:db:86-c5" in devices # media_player.living_room + assert "00:05:44:c2:d6:0a:e5:17-b7" in devices # media_player.bedroom + assert "00:f3:5f:fa:31:f3:32:21-a8" in devices # media_player.walkman + assert "00:b4:06:2e:91:95:23:97-fb" in devices # media_player.lounge_room + assert "00:b2:bd:f9:2c:ad:22:ae-58" in devices # fan.living_room_fan + assert "00:77:4c:8a:23:7d:27:4b-7f" not in devices # fan.ceiling_fan + assert "00:02:53:b9:d5:1a:b3:67-b2" in devices # cover.living_room_window + assert "00:42:03:fe:97:58:2d:b1-50" in devices # climate.hvac + assert "00:7b:2a:c7:08:d6:66:bf-80" in devices # climate.heatpump + assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee @asyncio.coroutine @@ -300,15 +300,15 @@ def test_get_light_state(hass_hue, hue_client): ) assert office_json["state"][HUE_API_STATE_ON] is False - assert office_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert office_json["state"][HUE_API_STATE_HUE] == 0 assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - yield from perform_get_light_state(hue_client, "light.bed_light", 404) + yield from perform_get_light_state(hue_client, "light.bed_light", 401) # Make sure kitchen light isn't accessible - yield from perform_get_light_state(hue_client, "light.kitchen_lights", 404) + yield from perform_get_light_state(hue_client, "light.kitchen_lights", 401) @asyncio.coroutine @@ -365,7 +365,7 @@ def test_put_light_state(hass_hue, hue_client): ceiling_json = yield from perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) - assert ceiling_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 assert ceiling_json["state"][HUE_API_STATE_SAT] == 0 @@ -373,7 +373,7 @@ def test_put_light_state(hass_hue, hue_client): bedroom_result = yield from perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 404 + assert bedroom_result.status == 401 # Make sure we can't change the kitchen light state kitchen_result = yield from perform_put_light_state( @@ -434,7 +434,7 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = yield from perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 404 + assert ecobee_result.status == 401 @asyncio.coroutine @@ -769,4 +769,4 @@ async def test_external_ip_blocked(hue_client): ): result = await hue_client.get("/api/username/lights") - assert result.status == 400 + assert result.status == 401