Improve emulated_hue compatibility with newer systems (#35148)

* Make emulated hue detectable by Busch-Jaeger free@home SysAP

* Emulated hue: Remove unnecessary host line from UPnP response

* Test that external IPs are blocked for config route

* Add another test for unauthorized users

* Change hue username to nouser

nouser seems to be used by the official Hue Bridge v1 Android app and is
used by other projects as well

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Thomas Hollstegge 2020-05-04 07:18:33 +02:00 committed by GitHub
parent 73fb57fd32
commit c5379a0f35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 8 deletions

View File

@ -14,6 +14,7 @@ from homeassistant.util.json import load_json, save_json
from .hue_api import (
HueAllGroupsStateView,
HueAllLightsStateView,
HueConfigView,
HueFullStateView,
HueGroupView,
HueOneLightChangeView,
@ -119,6 +120,7 @@ async def async_setup(hass, yaml_config):
HueAllGroupsStateView(config).register(app, app.router)
HueGroupView(config).register(app, app.router)
HueFullStateView(config).register(app, app.router)
HueConfigView(config).register(app, app.router)
upnp_listener = UPNPResponderThread(
config.host_ip_addr,

View File

@ -89,7 +89,7 @@ HUE_API_STATE_SAT_MAX = 254
HUE_API_STATE_CT_MIN = 153 # Color temp
HUE_API_STATE_CT_MAX = 500
HUE_API_USERNAME = "12345678901234567890"
HUE_API_USERNAME = "nouser"
UNAUTHORIZED_USER = [
{"error": {"address": "/", "description": "unauthorized user", "type": "1"}}
]
@ -226,14 +226,47 @@ class HueFullStateView(HomeAssistantView):
"config": {
"mac": "00:00:00:00:00:00",
"swversion": "01003542",
"apiversion": "1.17.0",
"whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}},
"ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}",
"linkbutton": True,
},
}
return self.json(json_response)
class HueConfigView(HomeAssistantView):
"""Return config view of emulated hue."""
url = "/api/{username}/config"
name = "emulated_hue:username:config"
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 configuration."""
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 = {
"mac": "00:00:00:00:00:00",
"swversion": "01003542",
"apiversion": "1.17.0",
"whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}},
"ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}",
"linkbutton": True,
}
return self.json(json_response)
class HueOneLightStateView(HomeAssistantView):
"""Handle requests for getting info about a single entity."""

View File

@ -42,7 +42,7 @@ class DescriptionXmlView(HomeAssistantView):
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>1234</serialNumber>
<serialNumber>001788FFFE23BFC2</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
</device>
</root>
@ -77,10 +77,10 @@ class UPNPResponderThread(threading.Thread):
CACHE-CONTROL: max-age=60
EXT:
LOCATION: http://{advertise_ip}:{advertise_port}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
hue-bridgeid: 1234
ST: urn:schemas-upnp-org:device:basic:1
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
hue-bridgeid: 001788FFFE23BFC2
ST: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
"""

View File

@ -26,6 +26,7 @@ from homeassistant.components.emulated_hue.hue_api import (
HUE_API_USERNAME,
HueAllGroupsStateView,
HueAllLightsStateView,
HueConfigView,
HueFullStateView,
HueOneLightChangeView,
HueOneLightStateView,
@ -191,6 +192,7 @@ def hue_client(loop, hass_hue, aiohttp_client):
HueOneLightChangeView(config).register(web_app, web_app.router)
HueAllGroupsStateView(config).register(web_app, web_app.router)
HueFullStateView(config).register(web_app, web_app.router)
HueConfigView(config).register(web_app, web_app.router)
return loop.run_until_complete(aiohttp_client(web_app))
@ -330,7 +332,7 @@ async def test_discover_full_state(hue_client):
# Make sure array is correct size
assert len(result_json) == 2
assert len(config_json) == 4
assert len(config_json) == 6
assert len(lights_json) >= 1
# Make sure the config wrapper added to the config is there
@ -341,6 +343,10 @@ async def test_discover_full_state(hue_client):
assert "swversion" in config_json
assert "01003542" in config_json["swversion"]
# Make sure the api version is correct
assert "apiversion" in config_json
assert "1.17.0" in config_json["apiversion"]
# Make sure the correct username in config
assert "whitelist" in config_json
assert HUE_API_USERNAME in config_json["whitelist"]
@ -351,6 +357,49 @@ async def test_discover_full_state(hue_client):
assert "ipaddress" in config_json
assert "127.0.0.1:8300" in config_json["ipaddress"]
# Make sure the device announces a link button
assert "linkbutton" in config_json
assert config_json["linkbutton"] is True
async def test_discover_config(hue_client):
"""Test the discovery of configuration."""
result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config")
assert result.status == 200
assert "application/json" in result.headers["content-type"]
config_json = await result.json()
# Make sure array is correct size
assert len(config_json) == 6
# 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 api version is correct
assert "apiversion" in config_json
assert "1.17.0" in config_json["apiversion"]
# 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"]
# Make sure the device announces a link button
assert "linkbutton" in config_json
assert config_json["linkbutton"] is True
async def test_get_light_state(hass_hue, hue_client):
"""Test the getting of light state."""
@ -905,6 +954,7 @@ async def test_external_ip_blocked(hue_client):
getUrls = [
"/api/username/groups",
"/api/username",
"/api/username/config",
"/api/username/lights",
"/api/username/lights/light.ceiling_lights",
]
@ -925,3 +975,17 @@ async def test_external_ip_blocked(hue_client):
for putUrl in putUrls:
result = await hue_client.put(putUrl)
assert result.status == HTTP_UNAUTHORIZED
async def test_unauthorized_user_blocked(hue_client):
"""Test unauthorized_user blocked."""
getUrls = [
"/api/wronguser",
"/api/wronguser/config",
]
for getUrl in getUrls:
result = await hue_client.get(getUrl)
assert result.status == HTTP_OK
result_json = await result.json()
assert result_json[0]["error"]["description"] == "unauthorized user"

View File

@ -57,7 +57,9 @@ class TestEmulatedHue(unittest.TestCase):
# Make sure the XML is parsable
try:
ET.fromstring(result.text)
root = ET.fromstring(result.text)
ns = {"s": "urn:schemas-upnp-org:device-1-0"}
assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2"
except: # noqa: E722 pylint: disable=bare-except
self.fail("description.xml is not valid XML!")