diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a45e58f28bc..95531450e5a 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -25,15 +25,18 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DEFAULT_SCAN_INTERVAL, DOMAIN, + IDENTIFIER_HOST, + IDENTIFIER_SERIAL_NUMBER, LOGGER, ) -from .device import Device, async_create_device, async_get_mac_address_from_host +from .device import Device, async_create_device NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" @@ -106,24 +109,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN] # Store mac address for changed UDN matching. - if device.host: - device.mac_address = await async_get_mac_address_from_host(hass, device.host) - if device.mac_address and not entry.data.get("CONFIG_ENTRY_MAC_ADDRESS"): + device_mac_address = await device.async_get_mac_address() + if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS): hass.config_entries.async_update_entry( entry=entry, data={ **entry.data, - CONFIG_ENTRY_MAC_ADDRESS: device.mac_address, + CONFIG_ENTRY_MAC_ADDRESS: device_mac_address, + CONFIG_ENTRY_HOST: device.host, }, ) + identifiers = {(DOMAIN, device.usn)} + if device.host: + identifiers.add((IDENTIFIER_HOST, device.host)) + if device.serial_number: + identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number)) + connections = {(dr.CONNECTION_UPNP, device.udn)} - if device.mac_address: - connections.add((dr.CONNECTION_NETWORK_MAC, device.mac_address)) + if device_mac_address: + connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address)) device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device( - identifiers=set(), connections=connections + identifiers=identifiers, connections=connections ) if device_entry: LOGGER.debug( @@ -136,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=connections, - identifiers={(DOMAIN, device.usn)}, + identifiers=identifiers, name=device.name, manufacturer=device.manufacturer, model=device.model_name, @@ -148,7 +157,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Update identifier. device_entry = device_registry.async_update_device( device_entry.id, - new_identifiers={(DOMAIN, device.usn)}, + new_identifiers=identifiers, ) assert device_entry diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7d4e768e855..3386cf40711 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -161,22 +162,25 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) + host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( # Store mac address for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, + CONFIG_ENTRY_HOST: host, }, ) # Handle devices changing their UDN, only allow a single host. for entry in self._async_current_entries(include_ignore=True): entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS) - entry_st = entry.data.get(CONFIG_ENTRY_ST) - if entry_mac_address != mac_address: + entry_host = entry.data.get(CONFIG_ENTRY_HOST) + if entry_mac_address != mac_address and entry_host != host: continue + entry_st = entry.data.get(CONFIG_ENTRY_ST) if discovery_info.ssdp_st != entry_st: # Check ssdp_st to prevent swapping between IGDv1 and IGDv2. continue diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index e673922d1c2..023ec82a487 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -6,7 +6,6 @@ from homeassistant.const import TIME_SECONDS LOGGER = logging.getLogger(__package__) -CONF_LOCAL_IP = "local_ip" DOMAIN = "upnp" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" @@ -24,7 +23,9 @@ CONFIG_ENTRY_UDN = "udn" CONFIG_ENTRY_ORIGINAL_UDN = "original_udn" CONFIG_ENTRY_MAC_ADDRESS = "mac_address" CONFIG_ENTRY_LOCATION = "location" +CONFIG_ENTRY_HOST = "host" +IDENTIFIER_HOST = "upnp_host" +IDENTIFIER_SERIAL_NUMBER = "upnp_serial_number" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30).total_seconds() ST_IGD_V1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ST_IGD_V2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2" -SSDP_SEARCH_TIMEOUT = 4 diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3a688b8571d..e06ada02b77 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -69,9 +69,15 @@ class Device: self.hass = hass self._igd_device = igd_device self.coordinator: DataUpdateCoordinator | None = None - self.mac_address: str | None = None self.original_udn: str | None = None + async def async_get_mac_address(self) -> str | None: + """Get mac address.""" + if not self.host: + return None + + return await async_get_mac_address_from_host(self.hass, self.host) + @property def udn(self) -> str: """Get the UDN.""" diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index e7cd24d0c7c..b159a371d9a 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -25,7 +25,7 @@ TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" TEST_LOCATION = "http://192.168.1.1/desc.xml" -TEST_HOSTNAME = urlparse(TEST_LOCATION).hostname +TEST_HOST = urlparse(TEST_LOCATION).hostname TEST_FRIENDLY_NAME = "mock-name" TEST_MAC_ADDRESS = "00:11:22:33:44:55" TEST_DISCOVERY = ssdp.SsdpServiceInfo( @@ -41,10 +41,11 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ssdp.ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, ssdp.ATTR_UPNP_MANUFACTURER: "mock-manufacturer", ssdp.ATTR_UPNP_MODEL_NAME: "mock-model-name", + ssdp.ATTR_UPNP_SERIAL: "mock-serial", ssdp.ATTR_UPNP_UDN: TEST_UDN, }, ssdp_headers={ - "_host": TEST_HOSTNAME, + "_host": TEST_HOST, }, ) @@ -54,8 +55,10 @@ def mock_igd_device() -> IgdDevice: """Mock async_upnp_client device.""" mock_upnp_device = create_autospec(UpnpDevice, instance=True) mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location + mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_SERIAL] mock_igd_device = create_autospec(IgdDevice) + mock_igd_device.device_type = TEST_DISCOVERY.ssdp_st mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index e89b8274c18..f0a1de1ce37 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( + CONFIG_ENTRY_HOST, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, @@ -21,6 +22,7 @@ from homeassistant.core import HomeAssistant from .conftest import ( TEST_DISCOVERY, TEST_FRIENDLY_NAME, + TEST_HOST, TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, @@ -140,7 +142,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): @pytest.mark.usefixtures("mock_mac_address_from_host") -async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): +async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant): """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -171,6 +173,38 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): assert result["reason"] == "config_entry_updated" +@pytest.mark.usefixtures("mock_mac_address_from_host") +async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant): + """Test config flow: discovery through ssdp, same device, but new UDN, matched on mac address.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_HOST: TEST_HOST, + }, + source=config_entries.SOURCE_SSDP, + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + # New discovery via step ssdp. + new_udn = TEST_UDN + "2" + new_discovery = deepcopy(TEST_DISCOVERY) + new_discovery.ssdp_usn = f"{new_udn}::{TEST_ST}" + new_discovery.upnp["_udn"] = new_udn + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=new_discovery, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "config_entry_updated" + + @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry",