Add full state view for emulated_hue (apps using emulated_hue, 'sleep cycle' and 'sleep as android') (#26650)

* Add full state view for emulated_hue

* clean and support updated sleep cycle

* emulated hue add reuasable logic and cleanup code

* emulated hue correct typos

* Update hue_api.py

* correct error message and update test_hue_api.py

* cleanup test_hue_api.py
This commit is contained in:
orrpan 2019-12-06 00:23:54 +01:00 committed by Paulus Schoutsen
parent 173966f459
commit 9ba9b3339b
4 changed files with 155 additions and 9 deletions

View file

@ -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,

View file

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

View file

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

View file

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