diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 791085b46f3..41c23707a03 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -14,11 +14,13 @@ from homeassistant.components.http import real_ip from .hue_api import ( HueUsernameView, + HueUnauthorizedUser, HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView, HueGroupView, HueAllGroupsStateView, + HueFullStateView, ) from .upnp import DescriptionXmlView, UPNPResponderThread @@ -113,11 +115,13 @@ async def async_setup(hass, yaml_config): DescriptionXmlView(config).register(app, app.router) HueUsernameView().register(app, app.router) + HueUnauthorizedUser().register(app, app.router) HueAllLightsStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router) HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) + HueFullStateView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index e7f15e7fc53..d7db6bb2fe3 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -89,6 +89,24 @@ HUE_API_STATE_SAT_MAX = 254 HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 +HUE_API_USERNAME = "12345678901234567890" +UNAUTHORIZED_USER = [ + {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} +] + + +class HueUnauthorizedUser(HomeAssistantView): + """Handle requests to find the emulated hue bridge.""" + + url = "/api" + name = "emulated_hue:api:unauthorized_user" + extra_urls = ["/api/"] + requires_auth = False + + async def get(self, request): + """Handle a GET request.""" + return self.json(UNAUTHORIZED_USER) + class HueUsernameView(HomeAssistantView): """Handle requests to create a username for the emulated hue bridge.""" @@ -111,7 +129,7 @@ class HueUsernameView(HomeAssistantView): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) + return self.json([{"success": {"username": HUE_API_USERNAME}}]) class HueAllGroupsStateView(HomeAssistantView): @@ -181,13 +199,37 @@ class HueAllLightsStateView(HomeAssistantView): if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) - hass = request.app["hass"] - json_response = {} + return self.json(create_list_of_entities(self.config, request)) - 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(self.config, entity) + +class HueFullStateView(HomeAssistantView): + """Return full state view of emulated hue.""" + + url = "/api/{username}" + name = "emulated_hue:username: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.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "lights": create_list_of_entities(self.config, request), + "config": { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + }, + } return self.json(json_response) @@ -673,3 +715,16 @@ def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} + + +def create_list_of_entities(config, request): + """Create a list of all entites.""" + hass = request.app["hass"] + json_response = {} + + for entity in hass.states.async_all(): + if config.is_entity_exposed(entity): + number = config.entity_id_to_number(entity.entity_id) + json_response[number] = entity_to_json(config, entity) + + return json_response diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 4f0d70d0046..749493a6ca8 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -25,11 +25,13 @@ from homeassistant.components.emulated_hue.hue_api import ( HUE_API_STATE_BRI, HUE_API_STATE_HUE, HUE_API_STATE_SAT, + HUE_API_USERNAME, HueUsernameView, HueOneLightStateView, HueAllLightsStateView, HueOneLightChangeView, HueAllGroupsStateView, + HueFullStateView, ) from homeassistant.const import STATE_ON, STATE_OFF @@ -188,6 +190,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightStateView(config).register(web_app, web_app.router) HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) + HueFullStateView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -252,6 +255,49 @@ def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): assert state_json["state"]["reachable"] == is_reachable, state_json +@asyncio.coroutine +def test_discover_full_state(hue_client): + """Test the discovery of full state.""" + result = yield from hue_client.get("/api/" + HUE_API_USERNAME) + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + result_json = yield from result.json() + + # Make sure array has correct content + assert "lights" in result_json + assert "lights" not in result_json["config"] + assert "config" in result_json + assert "config" not in result_json["lights"] + + lights_json = result_json["lights"] + config_json = result_json["config"] + + # Make sure array is correct size + assert len(result_json) == 2 + assert len(config_json) == 4 + assert len(lights_json) >= 1 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -763,10 +809,26 @@ def perform_put_light_state( async def test_external_ip_blocked(hue_client): """Test external IP blocked.""" + getUrls = [ + "/api/username/groups", + "/api/username", + "/api/username/lights", + "/api/username/lights/light.ceiling_lights", + ] + postUrls = ["/api"] + putUrls = ["/api/username/lights/light.ceiling_lights/state"] with patch( "homeassistant.components.http.real_ip.ip_address", return_value=ip_address("45.45.45.45"), ): - result = await hue_client.get("/api/username/lights") + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == 401 - assert result.status == 401 + for postUrl in postUrls: + result = await hue_client.post(postUrl) + assert result.status == 401 + + for putUrl in putUrls: + result = await hue_client.put(putUrl) + assert result.status == 401 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index ead78ad56ca..4cbc79f68a5 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -82,6 +82,31 @@ class TestEmulatedHue(unittest.TestCase): assert "success" in success_json assert "username" in success_json["success"] + def test_unauthorized_view(self): + """Test unauthorized view.""" + request_json = {"devicetype": "my_device"} + + result = requests.get( + BRIDGE_URL_BASE.format("/api/unauthorized"), + data=json.dumps(request_json), + timeout=5, + ) + + assert result.status_code == 200 + assert "application/json" in result.headers["content-type"] + + resp_json = result.json() + assert len(resp_json) == 1 + success_json = resp_json[0] + assert len(success_json) == 1 + + assert "error" in success_json + error_json = success_json["error"] + assert len(error_json) == 3 + assert "/" in error_json["address"] + assert "unauthorized user" in error_json["description"] + assert "1" in error_json["type"] + def test_valid_username_request(self): """Test request with a valid username.""" request_json = {"invalid_key": "my_device"}