Remove SSDP discovery from Hue (#85506)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
1afb4897a8
commit
fe583b7c4a
4 changed files with 3 additions and 276 deletions
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohue import LinkButtonNotPressed, create_app_key
|
from aiohue import LinkButtonNotPressed, create_app_key
|
||||||
|
@ -15,7 +14,7 @@ import slugify as unicode_slug
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import ssdp, zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
@ -201,53 +200,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
|
|
||||||
"""Handle a discovered Hue bridge.
|
|
||||||
|
|
||||||
This flow is triggered by the SSDP component. It will check if the
|
|
||||||
host is already configured and delegate to the import step if not.
|
|
||||||
"""
|
|
||||||
# Filter out non-Hue bridges #1
|
|
||||||
if (
|
|
||||||
discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL)
|
|
||||||
not in HUE_MANUFACTURERURL
|
|
||||||
):
|
|
||||||
return self.async_abort(reason="not_hue_bridge")
|
|
||||||
|
|
||||||
# Filter out non-Hue bridges #2
|
|
||||||
if any(
|
|
||||||
name in discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "")
|
|
||||||
for name in HUE_IGNORED_BRIDGE_NAMES
|
|
||||||
):
|
|
||||||
return self.async_abort(reason="not_hue_bridge")
|
|
||||||
|
|
||||||
if (
|
|
||||||
not discovery_info.ssdp_location
|
|
||||||
or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp
|
|
||||||
):
|
|
||||||
return self.async_abort(reason="not_hue_bridge")
|
|
||||||
|
|
||||||
url = urlparse(discovery_info.ssdp_location)
|
|
||||||
if not url.hostname:
|
|
||||||
return self.async_abort(reason="not_hue_bridge")
|
|
||||||
|
|
||||||
# Ignore if host is IPv6
|
|
||||||
if is_ipv6_address(url.hostname):
|
|
||||||
return self.async_abort(reason="invalid_host")
|
|
||||||
|
|
||||||
# abort if we already have exactly this bridge id/host
|
|
||||||
# reload the integration if the host got updated
|
|
||||||
bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL])
|
|
||||||
await self.async_set_unique_id(bridge_id)
|
|
||||||
self._abort_if_unique_id_configured(
|
|
||||||
updates={CONF_HOST: url.hostname}, reload_on_update=True
|
|
||||||
)
|
|
||||||
|
|
||||||
self.bridge = await self._get_bridge(
|
|
||||||
url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
|
|
||||||
)
|
|
||||||
return await self.async_step_link()
|
|
||||||
|
|
||||||
async def async_step_zeroconf(
|
async def async_step_zeroconf(
|
||||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
|
|
|
@ -4,20 +4,6 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/hue",
|
"documentation": "https://www.home-assistant.io/integrations/hue",
|
||||||
"requirements": ["aiohue==4.6.1"],
|
"requirements": ["aiohue==4.6.1"],
|
||||||
"ssdp": [
|
|
||||||
{
|
|
||||||
"manufacturer": "Royal Philips Electronics",
|
|
||||||
"modelName": "Philips hue bridge 2012"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"manufacturer": "Royal Philips Electronics",
|
|
||||||
"modelName": "Philips hue bridge 2015"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"manufacturer": "Signify",
|
|
||||||
"modelName": "Philips hue bridge 2015"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["BSB002"]
|
"models": ["BSB002"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -155,20 +155,6 @@ SSDP = {
|
||||||
"manufacturer": "SOYEA TECHNOLOGY CO., LTD.",
|
"manufacturer": "SOYEA TECHNOLOGY CO., LTD.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"hue": [
|
|
||||||
{
|
|
||||||
"manufacturer": "Royal Philips Electronics",
|
|
||||||
"modelName": "Philips hue bridge 2012",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"manufacturer": "Royal Philips Electronics",
|
|
||||||
"modelName": "Philips hue bridge 2015",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"manufacturer": "Signify",
|
|
||||||
"modelName": "Philips hue bridge 2015",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"hyperion": [
|
"hyperion": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Hyperion Open Source Ambient Lighting",
|
"manufacturer": "Hyperion Open Source Ambient Lighting",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import ssdp, zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.components.hue import config_flow, const
|
from homeassistant.components.hue import config_flow, const
|
||||||
from homeassistant.components.hue.errors import CannotConnect
|
from homeassistant.components.hue.errors import CannotConnect
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
@ -327,176 +327,6 @@ async def test_flow_link_cannot_connect(hass):
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mf_url", config_flow.HUE_MANUFACTURERURL)
|
|
||||||
async def test_bridge_ssdp(hass, mf_url, aioclient_mock):
|
|
||||||
"""Test a bridge being discovered."""
|
|
||||||
create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")])
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://0.0.0.0/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url,
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "form"
|
|
||||||
assert result["step_id"] == "link"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_discover_other_bridge(hass):
|
|
||||||
"""Test that discovery ignores other bridges."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
upnp={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "not_hue_bridge"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_emulated_hue(hass):
|
|
||||||
"""Test if discovery info is from an emulated hue instance."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://0.0.0.0/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge",
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "not_hue_bridge"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_missing_location(hass):
|
|
||||||
"""Test if discovery info is missing a location attribute."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "not_hue_bridge"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_missing_serial(hass):
|
|
||||||
"""Test if discovery info is a serial attribute."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://0.0.0.0/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "not_hue_bridge"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"location,reason",
|
|
||||||
(
|
|
||||||
("http:///", "not_hue_bridge"),
|
|
||||||
("http://[fd00::eeb5:faff:fe84:b17d]/description.xml", "invalid_host"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
async def test_bridge_ssdp_invalid_location(hass, location, reason):
|
|
||||||
"""Test if discovery info is a serial attribute."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location=location,
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == reason
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_espalexa(hass):
|
|
||||||
"""Test if discovery info is from an Espalexa based device."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://0.0.0.0/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)",
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "not_hue_bridge"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_bridge_ssdp_already_configured(hass, aioclient_mock):
|
|
||||||
"""Test if a discovered bridge has already been configured."""
|
|
||||||
create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")])
|
|
||||||
MockConfigEntry(
|
|
||||||
domain="hue", unique_id="1234", data={"host": "0.0.0.0"}
|
|
||||||
).add_to_hass(hass)
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://0.0.0.0/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "1234",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "already_configured"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_with_no_config(hass, aioclient_mock):
|
async def test_import_with_no_config(hass, aioclient_mock):
|
||||||
"""Test importing a host without an existing config file."""
|
"""Test importing a host without an existing config file."""
|
||||||
create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")])
|
create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")])
|
||||||
|
@ -634,33 +464,6 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock):
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_ssdp_discovery_update_configuration(hass, aioclient_mock):
|
|
||||||
"""Test if a discovered bridge is configured and updated with new host."""
|
|
||||||
create_mock_api_discovery(aioclient_mock, [("1.1.1.1", "aabbccddeeff")])
|
|
||||||
entry = MockConfigEntry(
|
|
||||||
domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"}
|
|
||||||
)
|
|
||||||
entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
const.DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_SSDP},
|
|
||||||
data=ssdp.SsdpServiceInfo(
|
|
||||||
ssdp_usn="mock_usn",
|
|
||||||
ssdp_st="mock_st",
|
|
||||||
ssdp_location="http://1.1.1.1/",
|
|
||||||
upnp={
|
|
||||||
ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0],
|
|
||||||
ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
|
||||||
assert result["reason"] == "already_configured"
|
|
||||||
assert entry.data["host"] == "1.1.1.1"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow_v1(hass):
|
async def test_options_flow_v1(hass):
|
||||||
"""Test options config flow for a V1 bridge."""
|
"""Test options config flow for a V1 bridge."""
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
|
@ -772,7 +575,7 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock):
|
||||||
)
|
)
|
||||||
entry = MockConfigEntry(
|
entry = MockConfigEntry(
|
||||||
domain="hue",
|
domain="hue",
|
||||||
source=config_entries.SOURCE_SSDP,
|
source=config_entries.SOURCE_HOMEKIT,
|
||||||
data={"host": "0.0.0.0"},
|
data={"host": "0.0.0.0"},
|
||||||
unique_id="ecb5faabcabc",
|
unique_id="ecb5faabcabc",
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue