mirror of
https://github.com/home-assistant/core.git
synced 2025-07-12 15:57:06 +00:00
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:
parent
e9334347eb
commit
39785c5cef
@ -1,14 +1,16 @@
|
|||||||
"""The SSDP integration."""
|
"""The SSDP integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from async_upnp_client.search import async_search
|
||||||
from defusedxml import ElementTree
|
from defusedxml import ElementTree
|
||||||
from netdisco import ssdp, util
|
from netdisco import ssdp, util
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
from homeassistant.loader import async_get_ssdp
|
from homeassistant.loader import async_get_ssdp
|
||||||
|
|
||||||
@ -51,12 +53,6 @@ async def async_setup(hass, config):
|
|||||||
return True
|
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 Scanner:
|
||||||
"""Class to manage SSDP scanning."""
|
"""Class to manage SSDP scanning."""
|
||||||
|
|
||||||
@ -64,25 +60,38 @@ class Scanner:
|
|||||||
"""Initialize class."""
|
"""Initialize class."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.seen = set()
|
self.seen = set()
|
||||||
|
self._entries = []
|
||||||
self._integration_matchers = integration_matchers
|
self._integration_matchers = integration_matchers
|
||||||
self._description_cache = {}
|
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, _):
|
async def async_scan(self, _):
|
||||||
"""Scan for new entries."""
|
"""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
|
# We clear the cache after each run. We track discovered entries
|
||||||
# so will never need a description twice.
|
# so will never need a description twice.
|
||||||
self._description_cache.clear()
|
self._description_cache.clear()
|
||||||
|
self._entries.clear()
|
||||||
|
|
||||||
async def _process_entries(self, entries):
|
async def _process_entries(self):
|
||||||
"""Process SSDP entries."""
|
"""Process SSDP entries."""
|
||||||
entries_to_process = []
|
entries_to_process = []
|
||||||
unseen_locations = set()
|
unseen_locations = set()
|
||||||
|
|
||||||
for entry in entries:
|
for entry in self._entries:
|
||||||
key = (entry.st, entry.location)
|
key = (entry.st, entry.location)
|
||||||
|
|
||||||
if key in self.seen:
|
if key in self.seen:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "ssdp",
|
"domain": "ssdp",
|
||||||
"name": "Simple Service Discovery Protocol (SSDP)",
|
"name": "Simple Service Discovery Protocol (SSDP)",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/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"],
|
"after_dependencies": ["zeroconf"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ PyNaCl==1.3.0
|
|||||||
aiohttp==3.7.3
|
aiohttp==3.7.3
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
astral==1.10.1
|
astral==1.10.1
|
||||||
|
async-upnp-client==0.14.13
|
||||||
async_timeout==3.0.1
|
async_timeout==3.0.1
|
||||||
attrs==19.3.0
|
attrs==19.3.0
|
||||||
awesomeversion==21.2.2
|
awesomeversion==21.2.2
|
||||||
|
@ -284,6 +284,7 @@ asmog==0.0.6
|
|||||||
asterisk_mbox==0.5.0
|
asterisk_mbox==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.dlna_dmr
|
# homeassistant.components.dlna_dmr
|
||||||
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
async-upnp-client==0.14.13
|
async-upnp-client==0.14.13
|
||||||
|
|
||||||
|
@ -173,6 +173,7 @@ aprslib==0.6.46
|
|||||||
arcam-fmj==0.5.3
|
arcam-fmj==0.5.3
|
||||||
|
|
||||||
# homeassistant.components.dlna_dmr
|
# homeassistant.components.dlna_dmr
|
||||||
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
async-upnp-client==0.14.13
|
async-upnp-client==0.14.13
|
||||||
|
|
||||||
|
@ -14,15 +14,18 @@ async def test_scan_match_st(hass, caplog):
|
|||||||
"""Test matching based on ST."""
|
"""Test matching based on ST."""
|
||||||
scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
|
scanner = ssdp.Scanner(hass, {"mock-domain": [{"st": "mock-st"}]})
|
||||||
|
|
||||||
with patch(
|
async def _inject_entry(*args, **kwargs):
|
||||||
"netdisco.ssdp.scan",
|
scanner.async_store_entry(
|
||||||
return_value=[
|
|
||||||
Mock(
|
Mock(
|
||||||
st="mock-st",
|
st="mock-st",
|
||||||
location=None,
|
location=None,
|
||||||
values={"usn": "mock-usn", "server": "mock-server", "ext": ""},
|
values={"usn": "mock-usn", "server": "mock-server", "ext": ""},
|
||||||
)
|
)
|
||||||
],
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.ssdp.async_search",
|
||||||
|
side_effect=_inject_entry,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||||
) as mock_init:
|
) 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"}]})
|
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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||||
) as mock_init:
|
) 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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||||
) as mock_init:
|
) 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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||||
) as mock_init:
|
) 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)
|
aioclient_mock.get("http://1.1.1.1", exc=exc)
|
||||||
scanner = ssdp.Scanner(hass, {})
|
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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
):
|
):
|
||||||
await scanner.async_scan(None)
|
await scanner.async_scan(None)
|
||||||
|
|
||||||
@ -165,9 +188,14 @@ async def test_scan_description_parse_fail(hass, aioclient_mock):
|
|||||||
)
|
)
|
||||||
scanner = ssdp.Scanner(hass, {})
|
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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
):
|
):
|
||||||
await scanner.async_scan(None)
|
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(
|
with patch(
|
||||||
"netdisco.ssdp.scan",
|
"homeassistant.components.ssdp.async_search",
|
||||||
return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})],
|
side_effect=_inject_entry,
|
||||||
), patch.object(
|
), patch.object(
|
||||||
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
hass.config_entries.flow, "async_init", return_value=mock_coro()
|
||||||
) as mock_init:
|
) as mock_init:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user