async def setup_platforms_and_discovery(self): """Set up platforms and discovery.""" await asyncio.gather( *( self.hass.config_entries.async_forward_entry_setup(self.entry, platform) for platform in PLATFORMS ) ) self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener ) ) _LOGGER.debug("Adding discovery job") if self.hosts: self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat ) ) await self.hass.async_add_executor_job(self._manual_hosts) return self.entry.async_on_unload( ssdp.async_register_callback( self.hass, self._async_ssdp_discovered_player, {"st": UPNP_ST} ) )
async def setup_platforms_and_discovery(): await asyncio.gather(*[ hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS ]) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_signal_update_groups)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_signal_update_alarms)) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_event_listener)) _LOGGER.debug("Adding discovery job") if hosts: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_manual_heartbeat)) await hass.async_add_executor_job(_manual_hosts) return entry.async_on_unload( ssdp.async_register_callback(hass, _async_discovered_player, {"st": UPNP_ST}))
async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: """Wait for a device to be discovered.""" device_discovered_event = asyncio.Event() @callback def device_discovered(info: Mapping[str, Any]) -> None: LOGGER.info( "Device discovered: %s, at: %s", info[ssdp.ATTR_SSDP_USN], info[ssdp.ATTR_SSDP_LOCATION], ) device_discovered_event.set() cancel_discovered_callback_1 = ssdp.async_register_callback( hass, device_discovered, { ssdp.ATTR_SSDP_ST: ST_IGD_V1, }, ) cancel_discovered_callback_2 = ssdp.async_register_callback( hass, device_discovered, { ssdp.ATTR_SSDP_ST: ST_IGD_V2, }, ) try: await asyncio.wait_for(device_discovered_event.wait(), timeout=SSDP_SEARCH_TIMEOUT) except asyncio.TimeoutError: return False finally: cancel_discovered_callback_1() cancel_discovered_callback_2() return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" LOGGER.debug("Setting up config entry: %s", entry.unique_id) udn = entry.data[CONFIG_ENTRY_UDN] st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name usn = f"{udn}::{st}" # Register device discovered-callback. device_discovered_event = asyncio.Event() discovery_info: Mapping[str, Any] | None = None @callback def device_discovered(info: Mapping[str, Any]) -> None: nonlocal discovery_info LOGGER.debug("Device discovered: %s, at: %s", usn, info[ssdp.ATTR_SSDP_LOCATION]) discovery_info = info device_discovered_event.set() cancel_discovered_callback = ssdp.async_register_callback( hass, device_discovered, { "usn": usn, }, ) try: await asyncio.wait_for(device_discovered_event.wait(), timeout=10) except asyncio.TimeoutError as err: LOGGER.debug("Device not discovered: %s", usn) raise ConfigEntryNotReady from err finally: cancel_discovered_callback() # Create device. location = discovery_info[ # pylint: disable=unsubscriptable-object ssdp.ATTR_SSDP_LOCATION] device = await Device.async_create_device(hass, location) # Ensure entry has a unique_id. if not entry.unique_id: LOGGER.debug( "Setting unique_id: %s, for config_entry: %s", device.unique_id, entry, ) hass.config_entries.async_update_entry( entry=entry, unique_id=device.unique_id, ) # Ensure entry has a hostname, for older entries. if (CONFIG_ENTRY_HOSTNAME not in entry.data or entry.data[CONFIG_ENTRY_HOSTNAME] != device.hostname): hass.config_entries.async_update_entry( entry=entry, data={ CONFIG_ENTRY_HOSTNAME: device.hostname, **entry.data }, ) # Create device registry entry. device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_UPNP, device.udn)}, identifiers={(DOMAIN, device.udn)}, name=device.name, manufacturer=device.manufacturer, model=device.model_name, ) update_interval_sec = entry.options.get(CONFIG_ENTRY_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) update_interval = timedelta(seconds=update_interval_sec) LOGGER.debug("update_interval: %s", update_interval) coordinator = UpnpDataUpdateCoordinator( hass, device=device, update_interval=update_interval, ) # Save coordinator. hass.data[DOMAIN][entry.entry_id] = coordinator await coordinator.async_config_entry_first_refresh() # Create sensors. LOGGER.debug("Enabling sensors") hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Start device updater. await device.async_start() return True
async def test_scan_second_hit(hass, aioclient_mock, caplog): """Test matching on second scan.""" aioclient_mock.get( "http://1.1.1.1", text=""" <root> <device> <deviceType>Paulus</deviceType> </device> </root> """, ) mock_ssdp_response = CaseInsensitiveDict( **{ "ST": "mock-st", "LOCATION": "http://1.1.1.1", "USN": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", "SERVER": "mock-server", "EXT": "", } ) mock_get_ssdp = {"mock-domain": [{"st": "mock-st"}]} integration_callbacks = [] @callback def _async_integration_callbacks(info): integration_callbacks.append(info) def _generate_fake_ssdp_listener(*args, **kwargs): listener = SSDPListener(*args, **kwargs) async def _async_callback(*_): pass @callback def _callback(*_): hass.async_create_task(listener.async_callback(mock_ssdp_response)) listener.async_start = _async_callback listener.async_search = _callback return listener with patch( "homeassistant.components.ssdp.async_get_ssdp", return_value=mock_get_ssdp, ), patch( "homeassistant.components.ssdp.SSDPListener", new=_generate_fake_ssdp_listener, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) await hass.async_block_till_done() remove = ssdp.async_register_callback( hass, _async_integration_callbacks, {"st": "mock-st"}, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() remove() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() assert len(integration_callbacks) == 4 assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_EXT: "", ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_ST: "mock-st", ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } 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": config_entries.SOURCE_SSDP } assert mock_init.mock_calls[0][2]["data"] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_ST: "mock-st", ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_EXT: "", ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } assert "Failed to fetch ssdp data" not in caplog.text udn_discovery_info = ssdp.async_get_discovery_info_by_st(hass, "mock-st") discovery_info = udn_discovery_info[0] assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" assert ( discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" ) assert ( discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" ) st_discovery_info = ssdp.async_get_discovery_info_by_udn( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" ) discovery_info = st_discovery_info[0] assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" assert ( discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" ) assert ( discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" ) discovery_info = ssdp.async_get_discovery_info_by_udn_st( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st" ) assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" assert ( discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" ) assert ( discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" ) assert ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None
async def test_unsolicited_ssdp_registered_callback(hass, aioclient_mock, caplog): """Test matching based on callback can handle unsolicited ssdp traffic without st.""" aioclient_mock.get( "http://10.6.9.12:1400/xml/device_description.xml", text=""" <root> <device> <deviceType>Paulus</deviceType> </device> </root> """, ) mock_ssdp_response = { "location": "http://10.6.9.12:1400/xml/device_description.xml", "nt": "uuid:RINCON_1111BB963FD801400", "nts": "ssdp:alive", "server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)", "usn": "uuid:RINCON_1111BB963FD801400", "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd", "x-rincon-bootseq": "250", "bootid.upnp.org": "250", "x-rincon-wifimode": "0", "x-rincon-variant": "1", "household.smartspeaker.audio": "Sonos_v3294823948542543534", } integration_callbacks = [] @callback def _async_integration_callbacks(info): integration_callbacks.append(info) def _generate_fake_ssdp_listener(*args, **kwargs): listener = SSDPListener(*args, **kwargs) async def _async_callback(*_): await listener.async_callback(mock_ssdp_response) @callback def _callback(*_): hass.async_create_task(listener.async_callback(mock_ssdp_response)) listener.async_start = _async_callback listener.async_search = _callback return listener with patch( "homeassistant.components.ssdp.SSDPListener", new=_generate_fake_ssdp_listener, ): hass.state = CoreState.stopped assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) await hass.async_block_till_done() ssdp.async_register_callback( hass, _async_integration_callbacks, {"nts": "ssdp:alive", "x-rincon-bootseq": MATCH_ALL}, ) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.state = CoreState.running await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() assert hass.state == CoreState.running assert ( len(integration_callbacks) == 4 ) # unsolicited callbacks without st are not cached assert integration_callbacks[0] == { "UDN": "uuid:RINCON_1111BB963FD801400", "bootid.upnp.org": "250", "deviceType": "Paulus", "household.smartspeaker.audio": "Sonos_v3294823948542543534", "nt": "uuid:RINCON_1111BB963FD801400", "nts": "ssdp:alive", "ssdp_location": "http://10.6.9.12:1400/xml/device_description.xml", "ssdp_server": "Linux UPnP/1.0 Sonos/63.2-88230 (ZPS12)", "ssdp_usn": "uuid:RINCON_1111BB963FD801400", "x-rincon-bootseq": "250", "x-rincon-household": "Sonos_dfjfkdghjhkjfhkdjfhkd", "x-rincon-variant": "1", "x-rincon-wifimode": "0", } assert "Failed to callback info" not in caplog.text
async def test_scan_with_registered_callback(hass, aioclient_mock, caplog): """Test matching based on callback.""" aioclient_mock.get( "http://1.1.1.1", text=""" <root> <device> <deviceType>Paulus</deviceType> </device> </root> """, ) mock_ssdp_response = { "st": "mock-st", "location": "http://1.1.1.1", "usn": "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", "server": "mock-server", "x-rincon-bootseq": "55", "ext": "", } not_matching_integration_callbacks = [] integration_match_all_callbacks = [] integration_match_all_not_present_callbacks = [] integration_callbacks = [] integration_callbacks_from_cache = [] match_any_callbacks = [] @callback def _async_exception_callbacks(info): raise ValueError @callback def _async_integration_callbacks(info): integration_callbacks.append(info) @callback def _async_integration_match_all_callbacks(info): integration_match_all_callbacks.append(info) @callback def _async_integration_match_all_not_present_callbacks(info): integration_match_all_not_present_callbacks.append(info) @callback def _async_integration_callbacks_from_cache(info): integration_callbacks_from_cache.append(info) @callback def _async_not_matching_integration_callbacks(info): not_matching_integration_callbacks.append(info) @callback def _async_match_any_callbacks(info): match_any_callbacks.append(info) def _generate_fake_ssdp_listener(*args, **kwargs): listener = SSDPListener(*args, **kwargs) async def _async_callback(*_): await listener.async_callback(mock_ssdp_response) @callback def _callback(*_): hass.async_create_task(listener.async_callback(mock_ssdp_response)) listener.async_start = _async_callback listener.async_search = _callback return listener with patch( "homeassistant.components.ssdp.SSDPListener", new=_generate_fake_ssdp_listener, ): hass.state = CoreState.stopped assert await async_setup_component(hass, ssdp.DOMAIN, {ssdp.DOMAIN: {}}) await hass.async_block_till_done() ssdp.async_register_callback(hass, _async_exception_callbacks, {}) ssdp.async_register_callback( hass, _async_integration_callbacks, {"st": "mock-st"}, ) ssdp.async_register_callback( hass, _async_integration_match_all_callbacks, {"x-rincon-bootseq": MATCH_ALL}, ) ssdp.async_register_callback( hass, _async_integration_match_all_not_present_callbacks, {"x-not-there": MATCH_ALL}, ) ssdp.async_register_callback( hass, _async_not_matching_integration_callbacks, {"st": "not-match-mock-st"}, ) ssdp.async_register_callback( hass, _async_match_any_callbacks, ) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) ssdp.async_register_callback( hass, _async_integration_callbacks_from_cache, {"st": "mock-st"}, ) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.state = CoreState.running await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() assert hass.state == CoreState.running assert len(integration_callbacks) == 5 assert len(integration_callbacks_from_cache) == 5 assert len(integration_match_all_callbacks) == 5 assert len(integration_match_all_not_present_callbacks) == 0 assert len(match_any_callbacks) == 5 assert len(not_matching_integration_callbacks) == 0 assert integration_callbacks[0] == { ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_SSDP_EXT: "", ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_ST: "mock-st", ssdp.ATTR_SSDP_USN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "x-rincon-bootseq": "55", } assert "Failed to callback info" in caplog.text