Switch ssdp to be async by using async_upnp_client for scanning (#46554)

SSDP scans no longer runs in the executor

This is an interim step that converts the async_upnp_client
response to netdisco's object to ensure fully backwards
compatibility
This commit is contained in:
J. Nick Koston 2021-02-18 00:00:11 -10:00 committed by GitHub
parent e9334347eb
commit 39785c5cef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 73 additions and 28 deletions

View File

@ -1,14 +1,16 @@
"""The SSDP integration."""
import asyncio
from datetime import timedelta
import itertools
import logging
from typing import Any, Mapping
import aiohttp
from async_upnp_client.search import async_search
from defusedxml import ElementTree
from netdisco import ssdp, util
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.loader import async_get_ssdp
@ -51,12 +53,6 @@ async def async_setup(hass, config):
return True
def _run_ssdp_scans():
_LOGGER.debug("Scanning")
# Run 3 times as packets can get lost
return itertools.chain.from_iterable([ssdp.scan() for _ in range(3)])
class Scanner:
"""Class to manage SSDP scanning."""
@ -64,25 +60,38 @@ class Scanner:
"""Initialize class."""
self.hass = hass
self.seen = set()
self._entries = []
self._integration_matchers = integration_matchers
self._description_cache = {}
async def _on_ssdp_response(self, data: Mapping[str, Any]) -> None:
"""Process an ssdp response."""
self.async_store_entry(
ssdp.UPNPEntry({key.lower(): item for key, item in data.items()})
)
@callback
def async_store_entry(self, entry):
"""Save an entry for later processing."""
self._entries.append(entry)
async def async_scan(self, _):
"""Scan for new entries."""
entries = await self.hass.async_add_executor_job(_run_ssdp_scans)
await self._process_entries(entries)
await async_search(async_callback=self._on_ssdp_response)
await self._process_entries()
# We clear the cache after each run. We track discovered entries
# so will never need a description twice.
self._description_cache.clear()
self._entries.clear()
async def _process_entries(self, entries):
async def _process_entries(self):
"""Process SSDP entries."""
entries_to_process = []
unseen_locations = set()
for entry in entries:
for entry in self._entries:
key = (entry.st, entry.location)
if key in self.seen:

View File

@ -2,7 +2,7 @@
"domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["defusedxml==0.6.0", "netdisco==2.8.2"],
"requirements": ["defusedxml==0.6.0", "netdisco==2.8.2", "async-upnp-client==0.14.13"],
"after_dependencies": ["zeroconf"],
"codeowners": []
}

View File

@ -3,6 +3,7 @@ PyNaCl==1.3.0
aiohttp==3.7.3
aiohttp_cors==0.7.0
astral==1.10.1
async-upnp-client==0.14.13
async_timeout==3.0.1
attrs==19.3.0
awesomeversion==21.2.2

View File

@ -284,6 +284,7 @@ asmog==0.0.6
asterisk_mbox==0.5.0
# homeassistant.components.dlna_dmr
# homeassistant.components.ssdp
# homeassistant.components.upnp
async-upnp-client==0.14.13

View File

@ -173,6 +173,7 @@ aprslib==0.6.46
arcam-fmj==0.5.3
# homeassistant.components.dlna_dmr
# homeassistant.components.ssdp
# homeassistant.components.upnp
async-upnp-client==0.14.13

View File

@ -14,15 +14,18 @@ async def test_scan_match_st(hass, caplog):
"""Test matching based on ST."""
scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
with patch(
"netdisco.ssdp.scan",
return_value=[
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(
st="mock-st",
location=None,
values={"usn": "mock-usn", "server": "mock-server", "ext": ""},
)
],
)
with patch(
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@ -58,9 +61,14 @@ async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key):
)
scanner = ssdp.Scanner(hass, {"mock-domain": [{key: "Paulus"}]})
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@ -95,9 +103,14 @@ async def test_scan_not_all_present(hass, aioclient_mock):
},
)
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@ -131,9 +144,14 @@ async def test_scan_not_all_match(hass, aioclient_mock):
},
)
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init:
@ -148,9 +166,14 @@ async def test_scan_description_fetch_fail(hass, aioclient_mock, exc):
aioclient_mock.get("http://1.1.1.1", exc=exc)
scanner = ssdp.Scanner(hass, {})
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
):
await scanner.async_scan(None)
@ -165,9 +188,14 @@ async def test_scan_description_parse_fail(hass, aioclient_mock):
)
scanner = ssdp.Scanner(hass, {})
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
):
await scanner.async_scan(None)
@ -196,9 +224,14 @@ async def test_invalid_characters(hass, aioclient_mock):
},
)
async def _inject_entry(*args, **kwargs):
scanner.async_store_entry(
Mock(st="mock-st", location="http://1.1.1.1", values={})
)
with patch(
"netdisco.ssdp.scan",
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
"homeassistant.components.ssdp.async_search",
side_effect=_inject_entry,
), patch.object(
hass.config_entries.flow, "async_init", return_value=mock_coro()
) as mock_init: