diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 3958ca9e3e5..ee81d1be88f 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -14,7 +14,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_TYPE, CONF_URL +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_HOST, + CONF_NAME, + CONF_TYPE, + CONF_URL, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError @@ -51,7 +57,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._discoveries: list[Mapping[str, str]] = [] + self._discoveries: dict[str, Mapping[str, Any]] = {} self._location: str | None = None self._udn: str | None = None self._device_type: str | None = None @@ -67,13 +73,43 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return DlnaDmrOptionsFlowHandler(config_entry) async def async_step_user(self, user_input: FlowInput = None) -> FlowResult: - """Handle a flow initialized by the user: manual URL entry. + """Handle a flow initialized by the user. - Discovered devices will already be displayed, no need to prompt user - with them here. + Let user choose from a list of found and unconfigured devices or to + enter an URL manually. """ LOGGER.debug("async_step_user: user_input: %s", user_input) + if user_input is not None: + host = user_input.get(CONF_HOST) + if not host: + # No device chosen, user might want to directly enter an URL + return await self.async_step_manual() + # User has chosen a device, ask for confirmation + discovery = self._discoveries[host] + await self._async_set_info_from_discovery(discovery) + return self._create_entry() + + discoveries = await self._async_get_discoveries() + if not discoveries: + # Nothing found, maybe the user knows an URL to try + return await self.async_step_manual() + + self._discoveries = { + discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(discovery[ssdp.ATTR_SSDP_LOCATION]).hostname: discovery + for discovery in discoveries + } + + data_schema = vol.Schema( + {vol.Optional(CONF_HOST): vol.In(self._discoveries.keys())} + ) + return self.async_show_form(step_id="user", data_schema=data_schema) + + async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: + """Manual URL entry by the user.""" + LOGGER.debug("async_step_manual: user_input: %s", user_input) + # Device setup manually, assume we don't get SSDP broadcast notifications self._options[CONF_POLL_AVAILABILITY] = True @@ -89,7 +125,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema({CONF_URL: str}) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="manual", data_schema=data_schema, errors=errors ) async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: @@ -177,6 +213,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_info_from_discovery(discovery_info) + if _is_ignored_device(discovery_info): + return self.async_abort(reason="alternative_integration") + # Abort if a migration flow for the device's location is in progress for progress in self._async_in_progress(include_uninitialized=True): if progress["context"].get("unique_id") == self._location: @@ -190,6 +229,29 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() + async def async_step_unignore(self, user_input: Mapping[str, Any]) -> FlowResult: + """Rediscover previously ignored devices by their unique_id.""" + LOGGER.debug("async_step_unignore: user_input: %s", user_input) + self._udn = user_input["unique_id"] + assert self._udn + await self.async_set_unique_id(self._udn) + + # Find a discovery matching the unignored unique_id for a DMR device + for dev_type in DmrDevice.DEVICE_TYPES: + discovery = await ssdp.async_get_discovery_info_by_udn_st( + self.hass, self._udn, dev_type + ) + if discovery: + break + else: + return self.async_abort(reason="discovery_error") + + await self._async_set_info_from_discovery(discovery, abort_if_configured=False) + + self.context["title_placeholders"] = {"name": self._name} + + return await self.async_step_confirm() + async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) @@ -213,7 +275,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: device = await domain_data.upnp_factory.async_create_device(self._location) except UpnpError as err: - raise ConnectError("could_not_connect") from err + raise ConnectError("cannot_connect") from err try: device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) @@ -284,12 +346,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or DEFAULT_NAME ) - async def _async_get_discoveries(self) -> list[Mapping[str, str]]: + async def _async_get_discoveries(self) -> list[Mapping[str, Any]]: """Get list of unconfigured DLNA devices discovered by SSDP.""" LOGGER.debug("_get_discoveries") # Get all compatible devices from ssdp's cache - discoveries: list[Mapping[str, str]] = [] + discoveries: list[Mapping[str, Any]] = [] for udn_st in DmrDevice.DEVICE_TYPES: st_discoveries = await ssdp.async_get_discovery_info_by_st( self.hass, udn_st @@ -298,7 +360,8 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Filter out devices already configured current_unique_ids = { - entry.unique_id for entry in self._async_current_entries() + entry.unique_id + for entry in self._async_current_entries(include_ignore=False) } discoveries = [ disc @@ -374,3 +437,25 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema(fields), errors=errors, ) + + +def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: + """Return True if this device should be ignored for discovery. + + These devices are supported better by other integrations, so don't bug + the user about them. The user can add them if desired by via the user config + flow, which will list all discovered but unconfigured devices. + """ + # Did the discovery trigger more than just this flow? + if len(discovery_info.get(ssdp.ATTR_HA_MATCHING_DOMAINS, set())) > 1: + LOGGER.debug( + "Ignoring device supported by multiple integrations: %s", + discovery_info[ssdp.ATTR_HA_MATCHING_DOMAINS], + ) + return True + + # Is the root device not a DMR? + if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: + return True + + return False diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 18e50ba1035..a7c0a674853 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -5,6 +5,32 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.22.9"], "dependencies": ["ssdp"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "st": "urn:schemas-upnp-org:device:MediaRenderer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" + } + ], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index c418305d2e6..ac6a35194fe 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -3,7 +3,14 @@ "flow_title": "{name}", "step": { "user": { - "title": "DLNA Digital Media Renderer", + "title": "Discovered DLNA DMR devices", + "description": "Choose a device to configure or leave blank to enter a URL", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "manual": { + "title": "Manual DLNA DMR device connection", "description": "URL to a device description XML file", "data": { "url": "[%key:common::config_flow::data::url%]" @@ -18,14 +25,15 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "could_not_connect": "Failed to connect to DLNA device", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", "not_dmr": "Device is not a Digital Media Renderer" }, "error": { - "could_not_connect": "Failed to connect to DLNA device", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "not_dmr": "Device is not a Digital Media Renderer" } }, diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 470328d0c27..3cdec814178 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Device is already configured", - "could_not_connect": "Failed to connect to DLNA device", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "Failed to connect", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", "not_dmr": "Device is not a Digital Media Renderer" }, "error": { - "could_not_connect": "Failed to connect to DLNA device", + "cannot_connect": "Failed to connect", "not_dmr": "Device is not a Digital Media Renderer" }, "flow_title": "{name}", @@ -20,12 +21,19 @@ "import_turn_on": { "description": "Please turn on the device and click submit to continue migration" }, - "user": { + "manual": { "data": { "url": "URL" }, "description": "URL to a device description XML file", - "title": "DLNA Digital Media Renderer" + "title": "Manual DLNA DMR device connection" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Choose a device to configure or leave blank to enter a URL", + "title": "Discovered DLNA DMR devices" } } }, diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index af3f09f560d..f5f4feaa70a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -54,6 +54,8 @@ ATTR_UPNP_SERIAL = "serialNumber" ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" +# Attributes for accessing info added by Home Assistant +ATTR_HA_MATCHING_DOMAINS = "x-homeassistant-matching-domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] @@ -398,6 +400,7 @@ class Scanner: return discovery_info = discovery_info_from_headers_and_description(info_with_desc) + discovery_info[ATTR_HA_MATCHING_DOMAINS] = matching_domains ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source] await _async_process_callbacks(callbacks, discovery_info, ssdp_change) diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1676037bbf2..925ba5b82fe 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -83,6 +83,32 @@ SSDP = { "manufacturer": "DIRECTV" } ], + "dlna_dmr": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "st": "urn:schemas-upnp-org:device:MediaRenderer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" + } + ], "fritz": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 0586a43422a..245d97be4aa 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.dlna_dmr.const import ( ) from homeassistant.const import ( CONF_DEVICE_ID, + CONF_HOST, CONF_NAME, CONF_PLATFORM, CONF_TYPE, @@ -49,26 +50,26 @@ MOCK_CONFIG_IMPORT_DATA = { } MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE" -MOCK_ROOT_DEVICE_TYPE = "ROOT_DEVICE_TYPE" MOCK_DISCOVERY = { ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, - ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_ROOT_DEVICE_TYPE, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + ssdp.ATTR_HA_MATCHING_DOMAINS: {DLNA_DOMAIN}, } -async def test_user_flow(hass: HomeAssistant) -> None: - """Test user-init'd config flow with user entering a valid URL.""" +async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: + """Test user-init'd flow, no discovered devices, user entering a valid URL.""" result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -87,6 +88,79 @@ async def test_user_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_user_flow_discovered_manual( + hass: HomeAssistant, ssdp_scanner_mock: Mock +) -> None: + """Test user-init'd flow, with discovered devices, user entering a valid URL.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {CONF_POLL_AVAILABILITY: True} + + # Wait for platform to be fully setup + await hass.async_block_till_done() + + +async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: + """Test user-init'd flow, user selects discovered device.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {} + + await hass.async_block_till_done() + + async def test_user_flow_uncontactable( hass: HomeAssistant, domain_data_mock: Mock ) -> None: @@ -99,15 +173,15 @@ async def test_user_flow_uncontactable( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "could_not_connect"} - assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + assert result["step_id"] == "manual" async def test_user_flow_embedded_st( @@ -117,7 +191,7 @@ async def test_user_flow_embedded_st( # Device is the wrong type upnp_device = domain_data_mock.upnp_factory.async_create_device.return_value upnp_device.udn = MOCK_ROOT_DEVICE_UDN - upnp_device.device_type = MOCK_ROOT_DEVICE_TYPE + upnp_device.device_type = "ROOT_DEVICE_TYPE" upnp_device.name = "ROOT_DEVICE_NAME" embedded_device = Mock(spec=UpnpDevice) embedded_device.udn = MOCK_DEVICE_UDN @@ -130,7 +204,7 @@ async def test_user_flow_embedded_st( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -160,7 +234,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -168,7 +242,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "not_dmr"} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" async def test_import_flow_invalid(hass: HomeAssistant, domain_data_mock: Mock) -> None: @@ -298,7 +372,7 @@ async def test_import_flow_offline( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "could_not_connect"} + assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "import_turn_on" # Device is discovered via SSDP, new flow should not be initialized @@ -469,7 +543,7 @@ async def test_ssdp_flow_upnp_udn( ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_UDN: "DIFFERENT_ROOT_DEVICE", - ssdp.ATTR_UPNP_DEVICE_TYPE: "DIFFERENT_ROOT_DEVICE_TYPE", + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, }, ) @@ -478,6 +552,108 @@ async def test_ssdp_flow_upnp_udn( assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION +async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: + """Test SSDP discovery ignores certain devices.""" + discovery = MOCK_DISCOVERY.copy() + discovery[ssdp.ATTR_HA_MATCHING_DOMAINS] = {DLNA_DOMAIN, "other_domain"} + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + + discovery = MOCK_DISCOVERY.copy() + discovery[ssdp.ATTR_UPNP_DEVICE_TYPE] = "urn:schemas-upnp-org:device:ZonePlayer:1" + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + + +async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: + """Test a config flow started by unignoring a device.""" + # Create ignored entry + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": MOCK_DEVICE_UDN, "title": MOCK_DEVICE_NAME}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == {} + + # Device was found via SSDP, matching the 2nd device type tried + ssdp_scanner_mock.async_get_discovery_info_by_udn_st.side_effect = [ + None, + MOCK_DISCOVERY, + None, + None, + None, + ] + + # Unignore it and expect config flow to start + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_UNIGNORE}, + data={"unique_id": MOCK_DEVICE_UDN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {} + + # Wait for platform to be fully setup + await hass.async_block_till_done() + + +async def test_unignore_flow_offline( + hass: HomeAssistant, ssdp_scanner_mock: Mock +) -> None: + """Test a config flow started by unignoring a device, but the device is offline.""" + # Create ignored entry + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": MOCK_DEVICE_UDN, "title": MOCK_DEVICE_NAME}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == {} + + # Device is not in the SSDP discoveries (perhaps HA restarted between ignore and unignore) + ssdp_scanner_mock.async_get_discovery_info_by_udn_st.return_value = None + + # Unignore it and expect config flow to start then abort + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_UNIGNORE}, + data={"unique_id": MOCK_DEVICE_UDN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "discovery_error" + + async def test_options_flow( hass: HomeAssistant, config_entry_mock: MockConfigEntry ) -> None: diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index e72b25715fe..c9098726ab4 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -71,6 +71,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", ssdp.ATTR_SSDP_UDN: ANY, "_timestamp": ANY, + ssdp.ATTR_HA_MATCHING_DOMAINS: {"mock-domain"}, } assert "Failed to fetch ssdp data" not in caplog.text @@ -463,6 +464,7 @@ async def test_scan_with_registered_callback( "x-rincon-bootseq": "55", ssdp.ATTR_SSDP_UDN: ANY, "_timestamp": ANY, + ssdp.ATTR_HA_MATCHING_DOMAINS: set(), }, ssdp.SsdpChange.ALIVE, )