"""Test UPnP/IGD setup process.""" from __future__ import annotations from collections.abc import Callable, Coroutine import copy from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from async_upnp_client.exceptions import UpnpCommunicationError from async_upnp_client.profiles.igd import IgdDevice import pytest from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_FORCE_POLL, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo from .conftest import ( TEST_DISCOVERY, TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, TEST_UDN, TEST_USN, ) from tests.common import MockConfigEntry @pytest.mark.usefixtures("ssdp_instant_discovery", "mock_mac_address_from_host") async def test_async_setup_entry_default( hass: HomeAssistant, mock_igd_device: IgdDevice ) -> None: """Test async_setup_entry.""" 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_MAC_ADDRESS: TEST_MAC_ADDRESS, }, options={ CONFIG_ENTRY_FORCE_POLL: False, }, ) # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True mock_igd_device.async_subscribe_services.assert_called() @pytest.mark.usefixtures("ssdp_instant_discovery", "mock_no_mac_address_from_host") async def test_async_setup_entry_default_no_mac_address(hass: HomeAssistant) -> None: """Test async_setup_entry.""" 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_MAC_ADDRESS: None, }, options={ CONFIG_ENTRY_FORCE_POLL: False, }, ) # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True @pytest.mark.usefixtures( "ssdp_instant_discovery_multi_location", "mock_mac_address_from_host", ) async def test_async_setup_entry_multi_location( hass: HomeAssistant, mock_async_create_device: AsyncMock ) -> None: """Test async_setup_entry for a device both seen via IPv4 and IPv6. The resulting IPv4 location is preferred/stored. """ 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_MAC_ADDRESS: TEST_MAC_ADDRESS, }, options={ CONFIG_ENTRY_FORCE_POLL: False, }, ) # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True # Ensure that the IPv4 location is used. mock_async_create_device.assert_called_once_with(TEST_LOCATION) @pytest.mark.usefixtures("mock_mac_address_from_host") async def test_async_setup_udn_mismatch( hass: HomeAssistant, mock_async_create_device: AsyncMock ) -> None: """Test async_setup_entry for a device which reports a different UDN from SSDP-discovery and device description.""" test_discovery = copy.deepcopy(TEST_DISCOVERY) test_discovery.upnp[ATTR_UPNP_UDN] = "uuid:another_udn" 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_MAC_ADDRESS: TEST_MAC_ADDRESS, }, options={ CONFIG_ENTRY_FORCE_POLL: False, }, ) # Set up device discovery callback. async def register_callback( hass: HomeAssistant, callback: Callable[ [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: """Immediately do callback.""" await callback(test_discovery, ssdp.SsdpChange.ALIVE) return MagicMock() with ( patch( "homeassistant.components.ssdp.async_register_callback", side_effect=register_callback, ), patch( "homeassistant.components.ssdp.async_get_discovery_info_by_st", return_value=[test_discovery], ), ): # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True # Ensure that the IPv4 location is used. mock_async_create_device.assert_called_once_with(TEST_LOCATION) @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_async_setup_entry_force_poll( hass: HomeAssistant, mock_igd_device: IgdDevice ) -> None: """Test async_setup_entry with forced polling.""" 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_MAC_ADDRESS: TEST_MAC_ADDRESS, }, options={ CONFIG_ENTRY_FORCE_POLL: True, }, ) # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True mock_igd_device.async_subscribe_services.assert_not_called() # Ensure that the device is forced to poll. mock_igd_device.async_get_traffic_and_status_data.assert_called_with( None, force_poll=True ) @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_get_source_ip", "mock_mac_address_from_host", ) async def test_async_setup_entry_force_poll_subscribe_error( hass: HomeAssistant, mock_igd_device: IgdDevice ) -> None: """Test async_setup_entry where subscribing fails.""" 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_MAC_ADDRESS: TEST_MAC_ADDRESS, }, options={ CONFIG_ENTRY_FORCE_POLL: False, }, ) # Subscribing partially succeeds, but not completely. # Unsubscribing will fail for the subscribed services afterwards. mock_igd_device.async_subscribe_services.side_effect = UpnpCommunicationError mock_igd_device.async_unsubscribe_services.side_effect = UpnpCommunicationError # Load config_entry, should still be able to load, falling back to polling/the old functionality. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True # Ensure that the device is forced to poll. mock_igd_device.async_get_traffic_and_status_data.assert_called_with( None, force_poll=True )