From 731f6180289c191112ea49cc2d6b2c5321edcfcf Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 15 Oct 2022 20:00:46 +0200 Subject: [PATCH] Make home assistant discoverable via UPnP/SSDP (#79820) --- .../components/dlna_dmr/manifest.json | 2 +- .../components/dlna_dms/manifest.json | 2 +- .../components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/__init__.py | 236 ++++++++++++++++-- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- .../components/yeelight/manifest.json | 2 +- homeassistant/components/yeelight/scanner.py | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/default_config/test_init.py | 4 +- tests/components/dlna_dmr/conftest.py | 9 +- tests/components/dlna_dms/conftest.py | 9 +- tests/components/samsungtv/conftest.py | 4 + tests/components/sonos/conftest.py | 4 + tests/components/ssdp/conftest.py | 10 + tests/components/ssdp/test_init.py | 20 +- tests/components/upnp/conftest.py | 4 + .../yamaha_musiccast/test_config_flow.py | 4 + 20 files changed, 290 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 7e03d34e900..582e48de839 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "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"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index a07f33d09dd..569b2cc635e 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "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"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 8523231e084..0fc989c57c6 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.31.2" + "async-upnp-client==0.32.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d221cb162f4..8783f5eeb34 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -8,27 +8,54 @@ from datetime import timedelta from enum import Enum from ipaddress import IPv4Address, IPv6Address import logging +import socket 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.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.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_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries 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.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.loader import async_get_ssdp, bind_hass DOMAIN = "ssdp" +SSDP_SCANNER = "scanner" +UPNP_SERVER = "server" +UPNP_SERVER_MIN_PORT = 40000 +UPNP_SERVER_MAX_PORT = 40100 SCAN_INTERVAL = timedelta(minutes=2) 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. """ - scanner: Scanner = hass.data[DOMAIN] + scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER] 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 ) -> SsdpServiceInfo | None: """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) @@ -151,7 +178,7 @@ async def async_get_discovery_info_by_st( # pylint: disable=invalid-name hass: HomeAssistant, st: str ) -> list[SsdpServiceInfo]: """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) @@ -160,19 +187,34 @@ async def async_get_discovery_info_by_udn( hass: HomeAssistant, udn: str ) -> list[SsdpServiceInfo]: """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) +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: """Set up the SSDP integration.""" integration_matchers = IntegrationMatchers() 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 @@ -322,14 +364,6 @@ class Scanner: 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: """Scan for new entries using ssdp listeners.""" await self.async_scan_multicast() @@ -369,7 +403,7 @@ class Scanner: """Start the SSDP Listeners.""" # Devices are shared between all sources. 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) if source_ip.version == 6: source_tuple: AddressTupleVXType = ( @@ -559,3 +593,171 @@ def _udn_from_usn(usn: str | None) -> str | None: if usn.startswith("uuid:"): return usn.split("::")[0] 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() diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index e403867226a..b74883e2008 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (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"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 8d1912f2fc4..b81b0ceb777 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "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"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1032ef0d2e5..ac688719fea 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "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"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 4c0b0f69310..7a0d409b434 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -76,7 +76,7 @@ class YeelightScanner: self._listeners.append( SsdpSearchListener( async_callback=self._async_process_entry, - service_type=SSDP_ST, + search_target=SSDP_ST, target=SSDP_TARGET, source=source, async_connect_callback=_wrap_async_connected_idx(idx), diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 97082d29719..57216b061f8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.31.2 +async-upnp-client==0.32.0 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 74dc226c22e..cb50784a4ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.31.2 +async-upnp-client==0.32.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b4bb8659e3..ab00afd91da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -307,7 +307,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.31.2 +async-upnp-client==0.32.0 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index f8f8c20dbb2..186e019fcbb 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -12,7 +12,9 @@ from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: @pytest.fixture(autouse=True) def 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 diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 84aec044caf..521f770a8fa 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -116,13 +116,20 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]: @pytest.fixture(autouse=True) 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: reg_callback = mock_scanner.return_value.async_register_callback reg_callback.return_value = Mock(return_value=None) 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) def async_get_local_ip_mock() -> Iterable[Mock]: """Mock the async_get_local_ip utility function to prevent network access.""" diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 4dcd135ea86..5b785fb4ba5 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -129,13 +129,20 @@ def dms_device_mock(upnp_factory_mock: Mock) -> Iterable[Mock]: @pytest.fixture(autouse=True) 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: reg_callback = mock_scanner.return_value.async_register_callback reg_callback.return_value = Mock(return_value=None) 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 async def device_source_mock( hass: HomeAssistant, diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 764022f3501..73ad642f7e7 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -32,6 +32,10 @@ async def silent_ssdp_scanner(hass): "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "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 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f776fb62d58..2ac1cb460cb 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -135,6 +135,10 @@ async def silent_ssdp_scanner(hass): "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "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 diff --git a/tests/components/ssdp/conftest.py b/tests/components/ssdp/conftest.py index 0b390ae469b..7b6d67895e5 100644 --- a/tests/components/ssdp/conftest.py +++ b/tests/components/ssdp/conftest.py @@ -1,6 +1,7 @@ """Configuration for SSDP tests.""" from unittest.mock import AsyncMock, patch +from async_upnp_client.server import UpnpServer from async_upnp_client.ssdp_listener import SsdpListener import pytest @@ -16,6 +17,15 @@ async def silent_ssdp_listener(): 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 def mock_flow_init(hass): """Mock hass.config_entries.flow.async_init.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index bf88f45acf9..e5d452af948 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from ipaddress import IPv4Address 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_listener import SsdpListener 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.""" await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) 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( @@ -407,7 +408,7 @@ async def test_discovery_from_advertisement_sets_ssdp_st( @patch( - "homeassistant.components.ssdp.Scanner._async_build_source_set", + "homeassistant.components.ssdp.async_build_source_set", return_value={IPv4Address("192.168.1.1")}, ) @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.""" 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} 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 SsdpListener.async_start = _async_start + UpnpServer.async_start = _async_start await init_ssdp_component(hass) 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} 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") @patch( diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index b159a371d9a..aee25a5d112 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -122,6 +122,10 @@ async def silent_ssdp_scanner(hass): "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "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 diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index a3fb0cf6211..b516bfe3843 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -21,6 +21,10 @@ async def silent_ssdp_scanner(hass): "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( "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