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