SSDP matching improvements (#28285)
* SSDP matching improvements - support multiple match groups per domain - require matches on all, not any item in a group - support matching on all UPnP device description data * Manifest structure fixes
This commit is contained in:
parent
a8dff2f2d0
commit
1679ec3245
10 changed files with 113 additions and 92 deletions
|
@ -6,11 +6,11 @@
|
|||
"requirements": [
|
||||
"pydeconz==64"
|
||||
],
|
||||
"ssdp": {
|
||||
"manufacturer": [
|
||||
"Royal Philips Electronics"
|
||||
]
|
||||
},
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@kane610"
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
"requirements": [
|
||||
"pyheos==0.6.0"
|
||||
],
|
||||
"ssdp": {
|
||||
"st": [
|
||||
"urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
]
|
||||
},
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
}
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@andrewsayre"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
"requirements": [
|
||||
"aiohue==1.9.2"
|
||||
],
|
||||
"ssdp": {
|
||||
"manufacturer": [
|
||||
"Royal Philips Electronics"
|
||||
]
|
||||
},
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"homekit": {
|
||||
"models": [
|
||||
"BSB002"
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
"pysonos==0.0.24"
|
||||
],
|
||||
"dependencies": [],
|
||||
"ssdp": {
|
||||
"st": [
|
||||
"urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
]
|
||||
},
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
}
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -104,28 +104,27 @@ class Scanner:
|
|||
|
||||
async def _process_entry(self, entry):
|
||||
"""Process a single entry."""
|
||||
domains = set(SSDP["st"].get(entry.st, []))
|
||||
|
||||
xml_location = entry.location
|
||||
info = {"st": entry.st}
|
||||
|
||||
if not xml_location:
|
||||
if domains:
|
||||
return (entry, info_from_entry(entry, None), domains)
|
||||
return None
|
||||
if entry.location:
|
||||
|
||||
# Multiple entries usually share same location. Make sure
|
||||
# we fetch it only once.
|
||||
info_req = self._description_cache.get(xml_location)
|
||||
# Multiple entries usually share same location. Make sure
|
||||
# we fetch it only once.
|
||||
info_req = self._description_cache.get(entry.location)
|
||||
|
||||
if info_req is None:
|
||||
info_req = self._description_cache[
|
||||
xml_location
|
||||
] = self.hass.async_create_task(self._fetch_description(xml_location))
|
||||
if info_req is None:
|
||||
info_req = self._description_cache[
|
||||
entry.location
|
||||
] = self.hass.async_create_task(self._fetch_description(entry.location))
|
||||
|
||||
info = await info_req
|
||||
info.update(await info_req)
|
||||
|
||||
domains.update(SSDP["manufacturer"].get(info.get("manufacturer"), []))
|
||||
domains.update(SSDP["device_type"].get(info.get("deviceType"), []))
|
||||
domains = set()
|
||||
for domain, matchers in SSDP.items():
|
||||
for matcher in matchers:
|
||||
if all(info.get(k) == v for (k, v) in matcher.items()):
|
||||
domains.add(domain)
|
||||
|
||||
if domains:
|
||||
return (entry, info_from_entry(entry, info), domains)
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
"requirements": [
|
||||
"pywemo==0.4.34"
|
||||
],
|
||||
"ssdp": {
|
||||
"manufacturer": [
|
||||
"Belkin International Inc."
|
||||
]
|
||||
},
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Belkin International Inc."
|
||||
}
|
||||
],
|
||||
"homekit": {
|
||||
"models": [
|
||||
"Wemo"
|
||||
|
|
|
@ -6,22 +6,29 @@ To update, run python3 -m script.hassfest
|
|||
# fmt: off
|
||||
|
||||
SSDP = {
|
||||
"device_type": {},
|
||||
"manufacturer": {
|
||||
"Belkin International Inc.": [
|
||||
"wemo"
|
||||
],
|
||||
"Royal Philips Electronics": [
|
||||
"deconz",
|
||||
"hue"
|
||||
]
|
||||
},
|
||||
"st": {
|
||||
"urn:schemas-denon-com:device:ACT-Denon:1": [
|
||||
"heos"
|
||||
],
|
||||
"urn:schemas-upnp-org:device:ZonePlayer:1": [
|
||||
"sonos"
|
||||
]
|
||||
}
|
||||
"deconz": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"heos": [
|
||||
{
|
||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
}
|
||||
],
|
||||
"hue": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"sonos": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||
}
|
||||
],
|
||||
"wemo": [
|
||||
{
|
||||
"manufacturer": "Belkin International Inc."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -14,11 +14,7 @@ MANIFEST_SCHEMA = vol.Schema(
|
|||
vol.Optional("config_flow"): bool,
|
||||
vol.Optional("zeroconf"): [str],
|
||||
vol.Optional("ssdp"): vol.Schema(
|
||||
{
|
||||
vol.Optional("st"): [str],
|
||||
vol.Optional("manufacturer"): [str],
|
||||
vol.Optional("device_type"): [str],
|
||||
}
|
||||
vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))])
|
||||
),
|
||||
vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}),
|
||||
vol.Required("documentation"): str,
|
||||
|
|
|
@ -24,11 +24,8 @@ def sort_dict(value):
|
|||
|
||||
def generate_and_validate(integrations: Dict[str, Integration]):
|
||||
"""Validate and generate ssdp data."""
|
||||
data = {
|
||||
"st": defaultdict(list),
|
||||
"manufacturer": defaultdict(list),
|
||||
"device_type": defaultdict(list),
|
||||
}
|
||||
|
||||
data = defaultdict(list)
|
||||
|
||||
for domain in sorted(integrations):
|
||||
integration = integrations[domain]
|
||||
|
@ -56,14 +53,9 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
|||
)
|
||||
continue
|
||||
|
||||
for key in "st", "manufacturer", "device_type":
|
||||
if key not in ssdp:
|
||||
continue
|
||||
for matcher in ssdp:
|
||||
data[domain].append(sort_dict(matcher))
|
||||
|
||||
for value in ssdp[key]:
|
||||
data[key][value].append(domain)
|
||||
|
||||
data = sort_dict({key: sort_dict(value) for key, value in data.items()})
|
||||
return BASE.format(json.dumps(data, indent=4))
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ async def test_scan_match_st(hass):
|
|||
|
||||
with patch(
|
||||
"netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location=None)]
|
||||
), patch.dict(gn_ssdp.SSDP["st"], {"mock-st": ["mock-domain"]}), patch.object(
|
||||
), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{"st": "mock-st"}]}), patch.object(
|
||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||
) as mock_init:
|
||||
await scanner.async_scan(None)
|
||||
|
@ -27,14 +27,15 @@ async def test_scan_match_st(hass):
|
|||
assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"}
|
||||
|
||||
|
||||
async def test_scan_match_manufacturer(hass, aioclient_mock):
|
||||
"""Test matching based on ST."""
|
||||
@pytest.mark.parametrize("key", ("manufacturer", "deviceType"))
|
||||
async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key):
|
||||
"""Test matching based on UPnP device description data."""
|
||||
aioclient_mock.get(
|
||||
"http://1.1.1.1",
|
||||
text="""
|
||||
text=f"""
|
||||
<root>
|
||||
<device>
|
||||
<manufacturer>Paulus</manufacturer>
|
||||
<{key}>Paulus</{key}>
|
||||
</device>
|
||||
</root>
|
||||
""",
|
||||
|
@ -44,9 +45,7 @@ async def test_scan_match_manufacturer(hass, aioclient_mock):
|
|||
with patch(
|
||||
"netdisco.ssdp.scan",
|
||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
|
||||
), patch.dict(
|
||||
gn_ssdp.SSDP["manufacturer"], {"Paulus": ["mock-domain"]}
|
||||
), patch.object(
|
||||
), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{key: "Paulus"}]}), patch.object(
|
||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||
) as mock_init:
|
||||
await scanner.async_scan(None)
|
||||
|
@ -56,11 +55,11 @@ async def test_scan_match_manufacturer(hass, aioclient_mock):
|
|||
assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"}
|
||||
|
||||
|
||||
async def test_scan_match_device_type(hass, aioclient_mock):
|
||||
"""Test matching based on ST."""
|
||||
async def test_scan_not_all_present(hass, aioclient_mock):
|
||||
"""Test match fails if some specified attributes are not present."""
|
||||
aioclient_mock.get(
|
||||
"http://1.1.1.1",
|
||||
text="""
|
||||
text=f"""
|
||||
<root>
|
||||
<device>
|
||||
<deviceType>Paulus</deviceType>
|
||||
|
@ -74,15 +73,43 @@ async def test_scan_match_device_type(hass, aioclient_mock):
|
|||
"netdisco.ssdp.scan",
|
||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
|
||||
), patch.dict(
|
||||
gn_ssdp.SSDP["device_type"], {"Paulus": ["mock-domain"]}
|
||||
gn_ssdp.SSDP,
|
||||
{"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Paulus"}]},
|
||||
), patch.object(
|
||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||
) as mock_init:
|
||||
await scanner.async_scan(None)
|
||||
|
||||
assert len(mock_init.mock_calls) == 1
|
||||
assert mock_init.mock_calls[0][1][0] == "mock-domain"
|
||||
assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"}
|
||||
assert not mock_init.mock_calls
|
||||
|
||||
|
||||
async def test_scan_not_all_match(hass, aioclient_mock):
|
||||
"""Test match fails if some specified attribute values differ."""
|
||||
aioclient_mock.get(
|
||||
"http://1.1.1.1",
|
||||
text=f"""
|
||||
<root>
|
||||
<device>
|
||||
<deviceType>Paulus</deviceType>
|
||||
<manufacturer>Paulus</manufacturer>
|
||||
</device>
|
||||
</root>
|
||||
""",
|
||||
)
|
||||
scanner = ssdp.Scanner(hass)
|
||||
|
||||
with patch(
|
||||
"netdisco.ssdp.scan",
|
||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1")],
|
||||
), patch.dict(
|
||||
gn_ssdp.SSDP,
|
||||
{"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Not-Paulus"}]},
|
||||
), patch.object(
|
||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||
) as mock_init:
|
||||
await scanner.async_scan(None)
|
||||
|
||||
assert not mock_init.mock_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exc", [asyncio.TimeoutError, aiohttp.ClientError])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue