mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Make home assistant discoverable via UPnP/SSDP (#79820)
This commit is contained in:
parent
d12cbab6c4
commit
731f618028
@ -3,7 +3,7 @@
|
|||||||
"name": "DLNA Digital Media Renderer",
|
"name": "DLNA Digital Media Renderer",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||||
"requirements": ["async-upnp-client==0.31.2"],
|
"requirements": ["async-upnp-client==0.32.0"],
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"after_dependencies": ["media_source"],
|
"after_dependencies": ["media_source"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "DLNA Digital Media Server",
|
"name": "DLNA Digital Media Server",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||||
"requirements": ["async-upnp-client==0.31.2"],
|
"requirements": ["async-upnp-client==0.32.0"],
|
||||||
"dependencies": ["ssdp"],
|
"dependencies": ["ssdp"],
|
||||||
"after_dependencies": ["media_source"],
|
"after_dependencies": ["media_source"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"samsungctl[websocket]==0.7.1",
|
"samsungctl[websocket]==0.7.1",
|
||||||
"samsungtvws[async,encrypted]==2.5.0",
|
"samsungtvws[async,encrypted]==2.5.0",
|
||||||
"wakeonlan==2.1.0",
|
"wakeonlan==2.1.0",
|
||||||
"async-upnp-client==0.31.2"
|
"async-upnp-client==0.32.0"
|
||||||
],
|
],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
|
@ -8,27 +8,54 @@ from datetime import timedelta
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from async_upnp_client.aiohttp import AiohttpSessionRequester
|
from async_upnp_client.aiohttp import AiohttpSessionRequester
|
||||||
from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource
|
from async_upnp_client.const import (
|
||||||
|
AddressTupleVXType,
|
||||||
|
DeviceIcon,
|
||||||
|
DeviceInfo,
|
||||||
|
DeviceOrServiceType,
|
||||||
|
SsdpSource,
|
||||||
|
)
|
||||||
from async_upnp_client.description_cache import DescriptionCache
|
from async_upnp_client.description_cache import DescriptionCache
|
||||||
|
from async_upnp_client.server import (
|
||||||
|
SSDP_SEARCH_RESPONDER_OPTION_ALWAYS_REPLY_WITH_ROOT_DEVICE,
|
||||||
|
SSDP_SEARCH_RESPONDER_OPTIONS,
|
||||||
|
UpnpServer,
|
||||||
|
UpnpServerDevice,
|
||||||
|
UpnpServerService,
|
||||||
|
)
|
||||||
from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address
|
from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address
|
||||||
from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener
|
from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener
|
||||||
from async_upnp_client.utils import CaseInsensitiveDict
|
from async_upnp_client.utils import CaseInsensitiveDict
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import network
|
from homeassistant.components import network
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, MATCH_ALL
|
from homeassistant.const import (
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
MATCH_ALL,
|
||||||
|
__version__ as current_version,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback as core_callback
|
from homeassistant.core import HomeAssistant, callback as core_callback
|
||||||
from homeassistant.data_entry_flow import BaseServiceInfo
|
from homeassistant.data_entry_flow import BaseServiceInfo
|
||||||
from homeassistant.helpers import discovery_flow
|
from homeassistant.helpers import discovery_flow
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||||
|
from homeassistant.helpers.network import get_url
|
||||||
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import async_get_ssdp, bind_hass
|
from homeassistant.loader import async_get_ssdp, bind_hass
|
||||||
|
|
||||||
DOMAIN = "ssdp"
|
DOMAIN = "ssdp"
|
||||||
|
SSDP_SCANNER = "scanner"
|
||||||
|
UPNP_SERVER = "server"
|
||||||
|
UPNP_SERVER_MIN_PORT = 40000
|
||||||
|
UPNP_SERVER_MAX_PORT = 40100
|
||||||
SCAN_INTERVAL = timedelta(minutes=2)
|
SCAN_INTERVAL = timedelta(minutes=2)
|
||||||
|
|
||||||
IPV4_BROADCAST = IPv4Address("255.255.255.255")
|
IPV4_BROADCAST = IPv4Address("255.255.255.255")
|
||||||
@ -133,7 +160,7 @@ async def async_register_callback(
|
|||||||
|
|
||||||
Returns a callback that can be used to cancel the registration.
|
Returns a callback that can be used to cancel the registration.
|
||||||
"""
|
"""
|
||||||
scanner: Scanner = hass.data[DOMAIN]
|
scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
|
||||||
return await scanner.async_register_callback(callback, match_dict)
|
return await scanner.async_register_callback(callback, match_dict)
|
||||||
|
|
||||||
|
|
||||||
@ -142,7 +169,7 @@ async def async_get_discovery_info_by_udn_st( # pylint: disable=invalid-name
|
|||||||
hass: HomeAssistant, udn: str, st: str
|
hass: HomeAssistant, udn: str, st: str
|
||||||
) -> SsdpServiceInfo | None:
|
) -> SsdpServiceInfo | None:
|
||||||
"""Fetch the discovery info cache."""
|
"""Fetch the discovery info cache."""
|
||||||
scanner: Scanner = hass.data[DOMAIN]
|
scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
|
||||||
return await scanner.async_get_discovery_info_by_udn_st(udn, st)
|
return await scanner.async_get_discovery_info_by_udn_st(udn, st)
|
||||||
|
|
||||||
|
|
||||||
@ -151,7 +178,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name
|
|||||||
hass: HomeAssistant, st: str
|
hass: HomeAssistant, st: str
|
||||||
) -> list[SsdpServiceInfo]:
|
) -> list[SsdpServiceInfo]:
|
||||||
"""Fetch all the entries matching the st."""
|
"""Fetch all the entries matching the st."""
|
||||||
scanner: Scanner = hass.data[DOMAIN]
|
scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
|
||||||
return await scanner.async_get_discovery_info_by_st(st)
|
return await scanner.async_get_discovery_info_by_st(st)
|
||||||
|
|
||||||
|
|
||||||
@ -160,19 +187,34 @@ async def async_get_discovery_info_by_udn(
|
|||||||
hass: HomeAssistant, udn: str
|
hass: HomeAssistant, udn: str
|
||||||
) -> list[SsdpServiceInfo]:
|
) -> list[SsdpServiceInfo]:
|
||||||
"""Fetch all the entries matching the udn."""
|
"""Fetch all the entries matching the udn."""
|
||||||
scanner: Scanner = hass.data[DOMAIN]
|
scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER]
|
||||||
return await scanner.async_get_discovery_info_by_udn(udn)
|
return await scanner.async_get_discovery_info_by_udn(udn)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6Address]:
|
||||||
|
"""Build the list of ssdp sources."""
|
||||||
|
return {
|
||||||
|
source_ip
|
||||||
|
for source_ip in await network.async_get_enabled_source_ips(hass)
|
||||||
|
if not source_ip.is_loopback and not source_ip.is_global
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the SSDP integration."""
|
"""Set up the SSDP integration."""
|
||||||
|
|
||||||
integration_matchers = IntegrationMatchers()
|
integration_matchers = IntegrationMatchers()
|
||||||
integration_matchers.async_setup(await async_get_ssdp(hass))
|
integration_matchers.async_setup(await async_get_ssdp(hass))
|
||||||
|
|
||||||
scanner = hass.data[DOMAIN] = Scanner(hass, integration_matchers)
|
scanner = Scanner(hass, integration_matchers)
|
||||||
|
server = Server(hass)
|
||||||
|
hass.data[DOMAIN] = {
|
||||||
|
SSDP_SCANNER: scanner,
|
||||||
|
UPNP_SERVER: server,
|
||||||
|
}
|
||||||
|
|
||||||
asyncio.create_task(scanner.async_start())
|
hass.create_task(scanner.async_start())
|
||||||
|
hass.create_task(server.async_start())
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -322,14 +364,6 @@ class Scanner:
|
|||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]:
|
|
||||||
"""Build the list of ssdp sources."""
|
|
||||||
return {
|
|
||||||
source_ip
|
|
||||||
for source_ip in await network.async_get_enabled_source_ips(self.hass)
|
|
||||||
if not source_ip.is_loopback and not source_ip.is_global
|
|
||||||
}
|
|
||||||
|
|
||||||
async def async_scan(self, *_: Any) -> None:
|
async def async_scan(self, *_: Any) -> None:
|
||||||
"""Scan for new entries using ssdp listeners."""
|
"""Scan for new entries using ssdp listeners."""
|
||||||
await self.async_scan_multicast()
|
await self.async_scan_multicast()
|
||||||
@ -369,7 +403,7 @@ class Scanner:
|
|||||||
"""Start the SSDP Listeners."""
|
"""Start the SSDP Listeners."""
|
||||||
# Devices are shared between all sources.
|
# Devices are shared between all sources.
|
||||||
device_tracker = SsdpDeviceTracker()
|
device_tracker = SsdpDeviceTracker()
|
||||||
for source_ip in await self._async_build_source_set():
|
for source_ip in await async_build_source_set(self.hass):
|
||||||
source_ip_str = str(source_ip)
|
source_ip_str = str(source_ip)
|
||||||
if source_ip.version == 6:
|
if source_ip.version == 6:
|
||||||
source_tuple: AddressTupleVXType = (
|
source_tuple: AddressTupleVXType = (
|
||||||
@ -559,3 +593,171 @@ def _udn_from_usn(usn: str | None) -> str | None:
|
|||||||
if usn.startswith("uuid:"):
|
if usn.startswith("uuid:"):
|
||||||
return usn.split("::")[0]
|
return usn.split("::")[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class HassUpnpServiceDevice(UpnpServerDevice):
|
||||||
|
"""Hass Device."""
|
||||||
|
|
||||||
|
DEVICE_DEFINITION = DeviceInfo(
|
||||||
|
device_type="urn:home-assistant.io:device:HomeAssistant:1",
|
||||||
|
friendly_name="filled_later_on",
|
||||||
|
manufacturer="Home Assistant",
|
||||||
|
manufacturer_url="https://www.home-assistant.io",
|
||||||
|
model_description=None,
|
||||||
|
model_name="filled_later_on",
|
||||||
|
model_number=current_version,
|
||||||
|
model_url="https://www.home-assistant.io",
|
||||||
|
serial_number="filled_later_on",
|
||||||
|
udn="filled_later_on",
|
||||||
|
upc=None,
|
||||||
|
presentation_url="https://my.home-assistant.io/",
|
||||||
|
url="/device.xml",
|
||||||
|
icons=[
|
||||||
|
DeviceIcon(
|
||||||
|
mimetype="image/png",
|
||||||
|
width=1024,
|
||||||
|
height=1024,
|
||||||
|
depth=24,
|
||||||
|
url="/static/icons/favicon-1024x1024.png",
|
||||||
|
),
|
||||||
|
DeviceIcon(
|
||||||
|
mimetype="image/png",
|
||||||
|
width=512,
|
||||||
|
height=512,
|
||||||
|
depth=24,
|
||||||
|
url="/static/icons/favicon-512x512.png",
|
||||||
|
),
|
||||||
|
DeviceIcon(
|
||||||
|
mimetype="image/png",
|
||||||
|
width=384,
|
||||||
|
height=384,
|
||||||
|
depth=24,
|
||||||
|
url="/static/icons/favicon-384x384.png",
|
||||||
|
),
|
||||||
|
DeviceIcon(
|
||||||
|
mimetype="image/png",
|
||||||
|
width=192,
|
||||||
|
height=192,
|
||||||
|
depth=24,
|
||||||
|
url="/static/icons/favicon-192x192.png",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
xml=ET.Element("server_device"),
|
||||||
|
)
|
||||||
|
EMBEDDED_DEVICES: list[type[UpnpServerDevice]] = []
|
||||||
|
SERVICES: list[type[UpnpServerService]] = []
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_find_next_available_port(source: AddressTupleVXType) -> int:
|
||||||
|
"""Get a free TCP port."""
|
||||||
|
family = socket.AF_INET if is_ipv4_address(source) else socket.AF_INET6
|
||||||
|
test_socket = socket.socket(family, socket.SOCK_STREAM)
|
||||||
|
test_socket.setblocking(False)
|
||||||
|
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
|
||||||
|
for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT):
|
||||||
|
try:
|
||||||
|
test_socket.bind(source)
|
||||||
|
return port
|
||||||
|
except OSError:
|
||||||
|
if port == UPNP_SERVER_MAX_PORT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
raise RuntimeError("unreachable")
|
||||||
|
|
||||||
|
|
||||||
|
class Server:
|
||||||
|
"""Class to be visible via SSDP searching and advertisements."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize class."""
|
||||||
|
self.hass = hass
|
||||||
|
self._upnp_servers: list[UpnpServer] = []
|
||||||
|
|
||||||
|
async def async_start(self) -> None:
|
||||||
|
"""Start the server."""
|
||||||
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop)
|
||||||
|
await self._async_start_upnp_servers()
|
||||||
|
|
||||||
|
async def _async_get_instance_udn(self) -> str:
|
||||||
|
"""Get Unique Device Name for this instance."""
|
||||||
|
instance_id = await async_get_instance_id(self.hass)
|
||||||
|
return f"uuid:{instance_id[0:8]}-{instance_id[8:12]}-{instance_id[12:16]}-{instance_id[16:20]}-{instance_id[20:32]}".upper()
|
||||||
|
|
||||||
|
async def _async_start_upnp_servers(self) -> None:
|
||||||
|
"""Start the UPnP/SSDP servers."""
|
||||||
|
# Update UDN with our instance UDN.
|
||||||
|
udn = await self._async_get_instance_udn()
|
||||||
|
system_info = await async_get_system_info(self.hass)
|
||||||
|
model_name = system_info["installation_type"]
|
||||||
|
presentation_url = get_url(self.hass)
|
||||||
|
serial_number = await async_get_instance_id(self.hass)
|
||||||
|
HassUpnpServiceDevice.DEVICE_DEFINITION = (
|
||||||
|
HassUpnpServiceDevice.DEVICE_DEFINITION._replace(
|
||||||
|
udn=udn,
|
||||||
|
friendly_name=f"{self.hass.config.location_name} (Home Assistant)",
|
||||||
|
model_name=model_name,
|
||||||
|
presentation_url=presentation_url,
|
||||||
|
serial_number=serial_number,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update icon URLs.
|
||||||
|
for index, icon in enumerate(HassUpnpServiceDevice.DEVICE_DEFINITION.icons):
|
||||||
|
new_url = urljoin(presentation_url, icon.url)
|
||||||
|
HassUpnpServiceDevice.DEVICE_DEFINITION.icons[index] = icon._replace(
|
||||||
|
url=new_url
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start a server on all source IPs.
|
||||||
|
for source_ip in await async_build_source_set(self.hass):
|
||||||
|
source_ip_str = str(source_ip)
|
||||||
|
if source_ip.version == 6:
|
||||||
|
source_tuple: AddressTupleVXType = (
|
||||||
|
source_ip_str,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
int(getattr(source_ip, "scope_id")),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
source_tuple = (source_ip_str, 0)
|
||||||
|
source, target = determine_source_target(source_tuple)
|
||||||
|
http_port = await _async_find_next_available_port(source)
|
||||||
|
_LOGGER.debug("Binding UPnP HTTP server to: %s:%s", source_ip, http_port)
|
||||||
|
self._upnp_servers.append(
|
||||||
|
UpnpServer(
|
||||||
|
source=source,
|
||||||
|
target=target,
|
||||||
|
http_port=http_port,
|
||||||
|
server_device=HassUpnpServiceDevice,
|
||||||
|
options={
|
||||||
|
SSDP_SEARCH_RESPONDER_OPTIONS: {
|
||||||
|
SSDP_SEARCH_RESPONDER_OPTION_ALWAYS_REPLY_WITH_ROOT_DEVICE: True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
results = await asyncio.gather(
|
||||||
|
*(upnp_server.async_start() for upnp_server in self._upnp_servers),
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
failed_servers = []
|
||||||
|
for idx, result in enumerate(results):
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Failed to setup server for %s: %s",
|
||||||
|
self._upnp_servers[idx].source,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
failed_servers.append(self._upnp_servers[idx])
|
||||||
|
for server in failed_servers:
|
||||||
|
self._upnp_servers.remove(server)
|
||||||
|
|
||||||
|
async def async_stop(self, *_: Any) -> None:
|
||||||
|
"""Stop the server."""
|
||||||
|
await self._async_stop_upnp_servers()
|
||||||
|
|
||||||
|
async def _async_stop_upnp_servers(self) -> None:
|
||||||
|
"""Stop UPnP/SSDP servers."""
|
||||||
|
for server in self._upnp_servers:
|
||||||
|
await server.async_stop()
|
||||||
|
@ -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": ["async-upnp-client==0.31.2"],
|
"requirements": ["async-upnp-client==0.32.0"],
|
||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
"after_dependencies": ["zeroconf"],
|
"after_dependencies": ["zeroconf"],
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "UPnP/IGD",
|
"name": "UPnP/IGD",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
||||||
"requirements": ["async-upnp-client==0.31.2", "getmac==0.8.2"],
|
"requirements": ["async-upnp-client==0.32.0", "getmac==0.8.2"],
|
||||||
"dependencies": ["network", "ssdp"],
|
"dependencies": ["network", "ssdp"],
|
||||||
"codeowners": ["@StevenLooman"],
|
"codeowners": ["@StevenLooman"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "yeelight",
|
"domain": "yeelight",
|
||||||
"name": "Yeelight",
|
"name": "Yeelight",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||||
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.2"],
|
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.0"],
|
||||||
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
|
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
|
@ -76,7 +76,7 @@ class YeelightScanner:
|
|||||||
self._listeners.append(
|
self._listeners.append(
|
||||||
SsdpSearchListener(
|
SsdpSearchListener(
|
||||||
async_callback=self._async_process_entry,
|
async_callback=self._async_process_entry,
|
||||||
service_type=SSDP_ST,
|
search_target=SSDP_ST,
|
||||||
target=SSDP_TARGET,
|
target=SSDP_TARGET,
|
||||||
source=source,
|
source=source,
|
||||||
async_connect_callback=_wrap_async_connected_idx(idx),
|
async_connect_callback=_wrap_async_connected_idx(idx),
|
||||||
|
@ -4,7 +4,7 @@ aiodiscover==1.4.13
|
|||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
astral==2.2
|
astral==2.2
|
||||||
async-upnp-client==0.31.2
|
async-upnp-client==0.32.0
|
||||||
async_timeout==4.0.2
|
async_timeout==4.0.2
|
||||||
atomicwrites-homeassistant==1.4.1
|
atomicwrites-homeassistant==1.4.1
|
||||||
attrs==21.2.0
|
attrs==21.2.0
|
||||||
|
@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
|
|||||||
# homeassistant.components.ssdp
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
async-upnp-client==0.31.2
|
async-upnp-client==0.32.0
|
||||||
|
|
||||||
# homeassistant.components.supla
|
# homeassistant.components.supla
|
||||||
asyncpysupla==0.0.5
|
asyncpysupla==0.0.5
|
||||||
|
@ -307,7 +307,7 @@ arcam-fmj==0.12.0
|
|||||||
# homeassistant.components.ssdp
|
# homeassistant.components.ssdp
|
||||||
# homeassistant.components.upnp
|
# homeassistant.components.upnp
|
||||||
# homeassistant.components.yeelight
|
# homeassistant.components.yeelight
|
||||||
async-upnp-client==0.31.2
|
async-upnp-client==0.32.0
|
||||||
|
|
||||||
# homeassistant.components.sleepiq
|
# homeassistant.components.sleepiq
|
||||||
asyncsleepiq==1.2.3
|
asyncsleepiq==1.2.3
|
||||||
|
@ -12,7 +12,9 @@ from tests.components.blueprint.conftest import stub_blueprint_populate # noqa:
|
|||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_ssdp():
|
def mock_ssdp():
|
||||||
"""Mock ssdp."""
|
"""Mock ssdp."""
|
||||||
with patch("homeassistant.components.ssdp.Scanner.async_scan"):
|
with patch("homeassistant.components.ssdp.Scanner.async_scan"), patch(
|
||||||
|
"homeassistant.components.ssdp.Server.async_start"
|
||||||
|
), patch("homeassistant.components.ssdp.Server.async_stop"):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@ -116,13 +116,20 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]:
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def ssdp_scanner_mock() -> Iterable[Mock]:
|
def ssdp_scanner_mock() -> Iterable[Mock]:
|
||||||
"""Mock the SSDP module."""
|
"""Mock the SSDP Scanner."""
|
||||||
with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
|
with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
|
||||||
reg_callback = mock_scanner.return_value.async_register_callback
|
reg_callback = mock_scanner.return_value.async_register_callback
|
||||||
reg_callback.return_value = Mock(return_value=None)
|
reg_callback.return_value = Mock(return_value=None)
|
||||||
yield mock_scanner.return_value
|
yield mock_scanner.return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def ssdp_server_mock() -> Iterable[Mock]:
|
||||||
|
"""Mock the SSDP Server."""
|
||||||
|
with patch("homeassistant.components.ssdp.Server", autospec=True):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def async_get_local_ip_mock() -> Iterable[Mock]:
|
def async_get_local_ip_mock() -> Iterable[Mock]:
|
||||||
"""Mock the async_get_local_ip utility function to prevent network access."""
|
"""Mock the async_get_local_ip utility function to prevent network access."""
|
||||||
|
@ -129,13 +129,20 @@ def dms_device_mock(upnp_factory_mock: Mock) -> Iterable[Mock]:
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def ssdp_scanner_mock() -> Iterable[Mock]:
|
def ssdp_scanner_mock() -> Iterable[Mock]:
|
||||||
"""Mock the SSDP module."""
|
"""Mock the SSDP Scanner."""
|
||||||
with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
|
with patch("homeassistant.components.ssdp.Scanner", autospec=True) as mock_scanner:
|
||||||
reg_callback = mock_scanner.return_value.async_register_callback
|
reg_callback = mock_scanner.return_value.async_register_callback
|
||||||
reg_callback.return_value = Mock(return_value=None)
|
reg_callback.return_value = Mock(return_value=None)
|
||||||
yield mock_scanner.return_value
|
yield mock_scanner.return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def ssdp_server_mock() -> Iterable[Mock]:
|
||||||
|
"""Mock the SSDP Server."""
|
||||||
|
with patch("homeassistant.components.ssdp.Server", autospec=True):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def device_source_mock(
|
async def device_source_mock(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -32,6 +32,10 @@ async def silent_ssdp_scanner(hass):
|
|||||||
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
||||||
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
||||||
"homeassistant.components.ssdp.Scanner.async_scan"
|
"homeassistant.components.ssdp.Scanner.async_scan"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_start_upnp_servers"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_stop_upnp_servers"
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@ -135,6 +135,10 @@ async def silent_ssdp_scanner(hass):
|
|||||||
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
||||||
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
||||||
"homeassistant.components.ssdp.Scanner.async_scan"
|
"homeassistant.components.ssdp.Scanner.async_scan"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_start_upnp_servers"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_stop_upnp_servers"
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Configuration for SSDP tests."""
|
"""Configuration for SSDP tests."""
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from async_upnp_client.server import UpnpServer
|
||||||
from async_upnp_client.ssdp_listener import SsdpListener
|
from async_upnp_client.ssdp_listener import SsdpListener
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -16,6 +17,15 @@ async def silent_ssdp_listener():
|
|||||||
yield SsdpListener
|
yield SsdpListener
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def disabled_upnp_server():
|
||||||
|
"""Disable UPnpServer."""
|
||||||
|
with patch("homeassistant.components.ssdp.UpnpServer.async_start"), patch(
|
||||||
|
"homeassistant.components.ssdp.UpnpServer.async_stop"
|
||||||
|
), patch("homeassistant.components.ssdp._async_find_next_available_port"):
|
||||||
|
yield UpnpServer
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_flow_init(hass):
|
def mock_flow_init(hass):
|
||||||
"""Mock hass.config_entries.flow.async_init."""
|
"""Mock hass.config_entries.flow.async_init."""
|
||||||
|
@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
|||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from unittest.mock import ANY, AsyncMock, patch
|
from unittest.mock import ANY, AsyncMock, patch
|
||||||
|
|
||||||
|
from async_upnp_client.server import UpnpServer
|
||||||
from async_upnp_client.ssdp import udn_from_headers
|
from async_upnp_client.ssdp import udn_from_headers
|
||||||
from async_upnp_client.ssdp_listener import SsdpListener
|
from async_upnp_client.ssdp_listener import SsdpListener
|
||||||
from async_upnp_client.utils import CaseInsensitiveDict
|
from async_upnp_client.utils import CaseInsensitiveDict
|
||||||
@ -34,7 +35,7 @@ async def init_ssdp_component(hass: homeassistant) -> SsdpListener:
|
|||||||
"""Initialize ssdp component and get SsdpListener."""
|
"""Initialize ssdp component and get SsdpListener."""
|
||||||
await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
return hass.data[ssdp.DOMAIN]._ssdp_listeners[0]
|
return hass.data[ssdp.DOMAIN][ssdp.SSDP_SCANNER]._ssdp_listeners[0]
|
||||||
|
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
@ -407,7 +408,7 @@ async def test_discovery_from_advertisement_sets_ssdp_st(
|
|||||||
|
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"homeassistant.components.ssdp.Scanner._async_build_source_set",
|
"homeassistant.components.ssdp.async_build_source_set",
|
||||||
return_value={IPv4Address("192.168.1.1")},
|
return_value={IPv4Address("192.168.1.1")},
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("mock_get_source_ip")
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
||||||
@ -668,7 +669,7 @@ async def test_async_detect_interfaces_setting_empty_route(
|
|||||||
"""Test without default interface config and the route returns nothing."""
|
"""Test without default interface config and the route returns nothing."""
|
||||||
await init_ssdp_component(hass)
|
await init_ssdp_component(hass)
|
||||||
|
|
||||||
ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
|
ssdp_listeners = hass.data[ssdp.DOMAIN][ssdp.SSDP_SCANNER]._ssdp_listeners
|
||||||
sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
|
sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
|
||||||
assert sources == {("2001:db8::%1", 0, 0, 1), ("192.168.1.5", 0)}
|
assert sources == {("2001:db8::%1", 0, 0, 1), ("192.168.1.5", 0)}
|
||||||
|
|
||||||
@ -698,14 +699,25 @@ async def test_bind_failure_skips_adapter(
|
|||||||
raise OSError
|
raise OSError
|
||||||
|
|
||||||
SsdpListener.async_start = _async_start
|
SsdpListener.async_start = _async_start
|
||||||
|
UpnpServer.async_start = _async_start
|
||||||
await init_ssdp_component(hass)
|
await init_ssdp_component(hass)
|
||||||
|
|
||||||
assert "Failed to setup listener for" in caplog.text
|
assert "Failed to setup listener for" in caplog.text
|
||||||
|
|
||||||
ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners
|
ssdp_listeners: list[SsdpListener] = hass.data[ssdp.DOMAIN][
|
||||||
|
ssdp.SSDP_SCANNER
|
||||||
|
]._ssdp_listeners
|
||||||
sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
|
sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners}
|
||||||
assert sources == {("192.168.1.5", 0)} # Note no SsdpListener for IPv6 address.
|
assert sources == {("192.168.1.5", 0)} # Note no SsdpListener for IPv6 address.
|
||||||
|
|
||||||
|
assert "Failed to setup server for" in caplog.text
|
||||||
|
|
||||||
|
upnp_servers: list[UpnpServer] = hass.data[ssdp.DOMAIN][
|
||||||
|
ssdp.UPNP_SERVER
|
||||||
|
]._upnp_servers
|
||||||
|
sources = {upnp_server.source for upnp_server in upnp_servers}
|
||||||
|
assert sources == {("192.168.1.5", 0)} # Note no UpnpServer for IPv6 address.
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("mock_get_source_ip")
|
@pytest.mark.usefixtures("mock_get_source_ip")
|
||||||
@patch(
|
@patch(
|
||||||
|
@ -122,6 +122,10 @@ async def silent_ssdp_scanner(hass):
|
|||||||
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
||||||
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
||||||
"homeassistant.components.ssdp.Scanner.async_scan"
|
"homeassistant.components.ssdp.Scanner.async_scan"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_start_upnp_servers"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_stop_upnp_servers"
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@ async def silent_ssdp_scanner(hass):
|
|||||||
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
"homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"
|
||||||
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch(
|
||||||
"homeassistant.components.ssdp.Scanner.async_scan"
|
"homeassistant.components.ssdp.Scanner.async_scan"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_start_upnp_servers"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.ssdp.Server._async_stop_upnp_servers"
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user