mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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:
parent
173966f459
commit
9ba9b3339b
@ -14,11 +14,13 @@ from homeassistant.components.http import real_ip
|
|||||||
|
|
||||||
from .hue_api import (
|
from .hue_api import (
|
||||||
HueUsernameView,
|
HueUsernameView,
|
||||||
|
HueUnauthorizedUser,
|
||||||
HueAllLightsStateView,
|
HueAllLightsStateView,
|
||||||
HueOneLightStateView,
|
HueOneLightStateView,
|
||||||
HueOneLightChangeView,
|
HueOneLightChangeView,
|
||||||
HueGroupView,
|
HueGroupView,
|
||||||
HueAllGroupsStateView,
|
HueAllGroupsStateView,
|
||||||
|
HueFullStateView,
|
||||||
)
|
)
|
||||||
from .upnp import DescriptionXmlView, UPNPResponderThread
|
from .upnp import DescriptionXmlView, UPNPResponderThread
|
||||||
|
|
||||||
@ -113,11 +115,13 @@ async def async_setup(hass, yaml_config):
|
|||||||
|
|
||||||
DescriptionXmlView(config).register(app, app.router)
|
DescriptionXmlView(config).register(app, app.router)
|
||||||
HueUsernameView().register(app, app.router)
|
HueUsernameView().register(app, app.router)
|
||||||
|
HueUnauthorizedUser().register(app, app.router)
|
||||||
HueAllLightsStateView(config).register(app, app.router)
|
HueAllLightsStateView(config).register(app, app.router)
|
||||||
HueOneLightStateView(config).register(app, app.router)
|
HueOneLightStateView(config).register(app, app.router)
|
||||||
HueOneLightChangeView(config).register(app, app.router)
|
HueOneLightChangeView(config).register(app, app.router)
|
||||||
HueAllGroupsStateView(config).register(app, app.router)
|
HueAllGroupsStateView(config).register(app, app.router)
|
||||||
HueGroupView(config).register(app, app.router)
|
HueGroupView(config).register(app, app.router)
|
||||||
|
HueFullStateView(config).register(app, app.router)
|
||||||
|
|
||||||
upnp_listener = UPNPResponderThread(
|
upnp_listener = UPNPResponderThread(
|
||||||
config.host_ip_addr,
|
config.host_ip_addr,
|
||||||
|
@ -89,6 +89,24 @@ HUE_API_STATE_SAT_MAX = 254
|
|||||||
HUE_API_STATE_CT_MIN = 153 # Color temp
|
HUE_API_STATE_CT_MIN = 153 # Color temp
|
||||||
HUE_API_STATE_CT_MAX = 500
|
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):
|
class HueUsernameView(HomeAssistantView):
|
||||||
"""Handle requests to create a username for the emulated hue bridge."""
|
"""Handle requests to create a username for the emulated hue bridge."""
|
||||||
@ -111,7 +129,7 @@ class HueUsernameView(HomeAssistantView):
|
|||||||
if "devicetype" not in data:
|
if "devicetype" not in data:
|
||||||
return self.json_message("devicetype not specified", HTTP_BAD_REQUEST)
|
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):
|
class HueAllGroupsStateView(HomeAssistantView):
|
||||||
@ -181,13 +199,37 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||||||
if not is_local(request[KEY_REAL_IP]):
|
if not is_local(request[KEY_REAL_IP]):
|
||||||
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
hass = request.app["hass"]
|
return self.json(create_list_of_entities(self.config, request))
|
||||||
json_response = {}
|
|
||||||
|
|
||||||
for entity in hass.states.async_all():
|
|
||||||
if self.config.is_entity_exposed(entity):
|
class HueFullStateView(HomeAssistantView):
|
||||||
number = self.config.entity_id_to_number(entity.entity_id)
|
"""Return full state view of emulated hue."""
|
||||||
json_response[number] = entity_to_json(self.config, entity)
|
|
||||||
|
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)
|
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."""
|
"""Create a success response for an attribute set on a light."""
|
||||||
success_key = f"/lights/{entity_id}/state/{attr}"
|
success_key = f"/lights/{entity_id}/state/{attr}"
|
||||||
return {"success": {success_key: value}}
|
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
|
||||||
|
@ -25,11 +25,13 @@ from homeassistant.components.emulated_hue.hue_api import (
|
|||||||
HUE_API_STATE_BRI,
|
HUE_API_STATE_BRI,
|
||||||
HUE_API_STATE_HUE,
|
HUE_API_STATE_HUE,
|
||||||
HUE_API_STATE_SAT,
|
HUE_API_STATE_SAT,
|
||||||
|
HUE_API_USERNAME,
|
||||||
HueUsernameView,
|
HueUsernameView,
|
||||||
HueOneLightStateView,
|
HueOneLightStateView,
|
||||||
HueAllLightsStateView,
|
HueAllLightsStateView,
|
||||||
HueOneLightChangeView,
|
HueOneLightChangeView,
|
||||||
HueAllGroupsStateView,
|
HueAllGroupsStateView,
|
||||||
|
HueFullStateView,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF
|
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)
|
HueOneLightStateView(config).register(web_app, web_app.router)
|
||||||
HueOneLightChangeView(config).register(web_app, web_app.router)
|
HueOneLightChangeView(config).register(web_app, web_app.router)
|
||||||
HueAllGroupsStateView(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))
|
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
|
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
|
@asyncio.coroutine
|
||||||
def test_get_light_state(hass_hue, hue_client):
|
def test_get_light_state(hass_hue, hue_client):
|
||||||
"""Test the getting of light state."""
|
"""Test the getting of light state."""
|
||||||
@ -763,10 +809,26 @@ def perform_put_light_state(
|
|||||||
|
|
||||||
async def test_external_ip_blocked(hue_client):
|
async def test_external_ip_blocked(hue_client):
|
||||||
"""Test external IP blocked."""
|
"""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(
|
with patch(
|
||||||
"homeassistant.components.http.real_ip.ip_address",
|
"homeassistant.components.http.real_ip.ip_address",
|
||||||
return_value=ip_address("45.45.45.45"),
|
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
|
||||||
|
|
||||||
|
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
|
assert result.status == 401
|
||||||
|
@ -82,6 +82,31 @@ class TestEmulatedHue(unittest.TestCase):
|
|||||||
assert "success" in success_json
|
assert "success" in success_json
|
||||||
assert "username" in success_json["success"]
|
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):
|
def test_valid_username_request(self):
|
||||||
"""Test request with a valid username."""
|
"""Test request with a valid username."""
|
||||||
request_json = {"invalid_key": "my_device"}
|
request_json = {"invalid_key": "my_device"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user