diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index 536567336fd..6a53490819f 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -3,39 +3,13 @@ from __future__ import annotations from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.const import CONF_PLATFORM, CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER +from .const import LOGGER PLATFORMS = [MEDIA_PLAYER_DOMAIN] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up DLNA component.""" - if MEDIA_PLAYER_DOMAIN not in config: - return True - - for entry_config in config[MEDIA_PLAYER_DOMAIN]: - if entry_config.get(CONF_PLATFORM) != DOMAIN: - continue - LOGGER.warning( - "Configuring dlna_dmr via yaml is deprecated; the configuration for" - " %s has been migrated to a config entry and can be safely removed", - entry_config.get(CONF_URL), - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_config, - ) - ) - - return True - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 53513d593f5..551faf2815f 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -25,6 +25,7 @@ from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, + DEFAULT_NAME, DOMAIN, ) from .data import get_domain_data @@ -51,6 +52,11 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" self._discoveries: list[Mapping[str, str]] = [] + self._location: str | None = None + self._udn: str | None = None + self._device_type: str | None = None + self._name: str | None = None + self._options: dict[str, Any] = {} @staticmethod @callback @@ -68,22 +74,18 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """ LOGGER.debug("async_step_user: user_input: %s", user_input) + # Device setup manually, assume we don't get SSDP broadcast notifications + self._options[CONF_POLL_AVAILABILITY] = True + errors = {} if user_input is not None: + self._location = user_input[CONF_URL] try: - discovery = await self._async_connect(user_input[CONF_URL]) + await self._async_connect() except ConnectError as err: errors["base"] = err.args[0] else: - # If unmigrated config was imported earlier then use it - import_data = get_domain_data(self.hass).unmigrated_config.get( - user_input[CONF_URL] - ) - if import_data is not None: - return await self.async_step_import(import_data) - # Device setup manually, assume we don't get SSDP broadcast notifications - options = {CONF_POLL_AVAILABILITY: True} - return await self._async_create_entry_from_discovery(discovery, options) + return self._create_entry() data_schema = vol.Schema({CONF_URL: str}) return self.async_show_form( @@ -93,9 +95,10 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: """Import a new DLNA DMR device from a config entry. - This flow is triggered by `async_setup`. If no device has been - configured before, find any matching device and create a config_entry - for it. Otherwise, do nothing. + This flow is triggered by `async_setup_platform`. If the device has not + been migrated, and can be connected to, automatically import it. If it + cannot be connected to, prompt the user to turn it on. If it has already + been migrated, do nothing. """ LOGGER.debug("async_step_import: import_data: %s", import_data) @@ -103,123 +106,183 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Entry not imported: incomplete_config") return self.async_abort(reason="incomplete_config") - self._async_abort_entries_match({CONF_URL: import_data[CONF_URL]}) + self._location = import_data[CONF_URL] + self._async_abort_entries_match({CONF_URL: self._location}) - location = import_data[CONF_URL] - self._discoveries = await self._async_get_discoveries() - - poll_availability = True - - # Find the device in the list of unconfigured devices - for discovery in self._discoveries: - if discovery[ssdp.ATTR_SSDP_LOCATION] == location: - # Device found via SSDP, it shouldn't need polling - poll_availability = False - LOGGER.debug( - "Entry %s found via SSDP, with UDN %s", - import_data[CONF_URL], - discovery[ssdp.ATTR_SSDP_UDN], - ) - break - else: - # Not in discoveries. Try connecting directly. - try: - discovery = await self._async_connect(location) - except ConnectError as err: - LOGGER.debug( - "Entry %s not imported: %s", import_data[CONF_URL], err.args[0] - ) - # Store the config to apply if the device is added later - get_domain_data(self.hass).unmigrated_config[location] = import_data - return self.async_abort(reason=err.args[0]) + # Use the location as this config flow's unique ID until UDN is known + await self.async_set_unique_id(self._location) # Set options from the import_data, except listen_ip which is no longer used - options = { - CONF_LISTEN_PORT: import_data.get(CONF_LISTEN_PORT), - CONF_CALLBACK_URL_OVERRIDE: import_data.get(CONF_CALLBACK_URL_OVERRIDE), - CONF_POLL_AVAILABILITY: poll_availability, - } + self._options[CONF_LISTEN_PORT] = import_data.get(CONF_LISTEN_PORT) + self._options[CONF_CALLBACK_URL_OVERRIDE] = import_data.get( + CONF_CALLBACK_URL_OVERRIDE + ) # Override device name if it's set in the YAML - if CONF_NAME in import_data: - discovery = dict(discovery) - discovery[ssdp.ATTR_UPNP_FRIENDLY_NAME] = import_data[CONF_NAME] + self._name = import_data.get(CONF_NAME) - LOGGER.debug("Entry %s ready for import", import_data[CONF_URL]) - return await self._async_create_entry_from_discovery(discovery, options) + discoveries = await self._async_get_discoveries() + + # Find the device in the list of unconfigured devices + for discovery in discoveries: + if discovery[ssdp.ATTR_SSDP_LOCATION] == self._location: + # Device found via SSDP, it shouldn't need polling + self._options[CONF_POLL_AVAILABILITY] = False + # Discovery info has everything required to create config entry + await self._async_set_info_from_discovery(discovery) + LOGGER.debug( + "Entry %s found via SSDP, with UDN %s", + self._location, + self._udn, + ) + return self._create_entry() + + # This device will need to be polled + self._options[CONF_POLL_AVAILABILITY] = True + + # Device was not found via SSDP, connect directly for configuration + try: + await self._async_connect() + except ConnectError as err: + # This will require user action + LOGGER.debug("Entry %s not imported yet: %s", self._location, err.args[0]) + return await self.async_step_import_turn_on() + + LOGGER.debug("Entry %s ready for import", self._location) + return self._create_entry() + + async def async_step_import_turn_on( + self, user_input: FlowInput = None + ) -> FlowResult: + """Request the user to turn on the device so that import can finish.""" + LOGGER.debug("async_step_import_turn_on: %s", user_input) + + self.context["title_placeholders"] = {"name": self._name or self._location} + + errors = {} + if user_input is not None: + try: + await self._async_connect() + except ConnectError as err: + errors["base"] = err.args[0] + else: + return self._create_entry() + + self._set_confirm_only() + return self.async_show_form(step_id="import_turn_on", errors=errors) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) - self._discoveries = [discovery_info] + await self._async_set_info_from_discovery(discovery_info) - udn = discovery_info[ssdp.ATTR_SSDP_UDN] - location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + # 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: + LOGGER.debug( + "Aborting SSDP setup because migration for %s is in progress", + self._location, + ) + return self.async_abort(reason="already_in_progress") - # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured( - updates={CONF_URL: location}, reload_on_update=False - ) - - # If the device needs migration because it wasn't turned on when HA - # started, silently migrate it now. - import_data = get_domain_data(self.hass).unmigrated_config.get(location) - if import_data is not None: - return await self.async_step_import(import_data) - - parsed_url = urlparse(location) - name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname - self.context["title_placeholders"] = {"name": name} + 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. - - Also check that the device is still available, otherwise when it is - added to HA it won't report the correct DeviceInfo. - """ + """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) - errors = {} if user_input is not None: - discovery = self._discoveries[0] - try: - await self._async_connect(discovery[ssdp.ATTR_SSDP_LOCATION]) - except ConnectError as err: - errors["base"] = err.args[0] - else: - return await self._async_create_entry_from_discovery(discovery) + return self._create_entry() self._set_confirm_only() - return self.async_show_form(step_id="confirm", errors=errors) + return self.async_show_form(step_id="confirm") - async def _async_create_entry_from_discovery( - self, - discovery: Mapping[str, Any], - options: Mapping[str, Any] | None = None, - ) -> FlowResult: - """Create an entry from discovery.""" - LOGGER.debug("_async_create_entry_from_discovery: discovery: %s", discovery) + async def _async_connect(self) -> None: + """Connect to a device to confirm it works and gather extra information. - location = discovery[ssdp.ATTR_SSDP_LOCATION] - udn = discovery[ssdp.ATTR_SSDP_UDN] + Updates this flow's unique ID to the device UDN if not already done. + Raises ConnectError if something goes wrong. + """ + LOGGER.debug("_async_connect: location: %s", self._location) + assert self._location, "self._location has not been set before connect" + + domain_data = get_domain_data(self.hass) + try: + device = await domain_data.upnp_factory.async_create_device(self._location) + except UpnpError as err: + raise ConnectError("could_not_connect") from err + + try: + device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) + except UpnpError as err: + raise ConnectError("not_dmr") from err + + if not self._udn: + self._udn = device.udn + await self.async_set_unique_id(self._udn) # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured(updates={CONF_URL: location}) + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) - parsed_url = urlparse(location) - title = discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname + if not self._device_type: + self._device_type = device.device_type + if not self._name: + self._name = device.name + + def _create_entry(self) -> FlowResult: + """Create a config entry, assuming all required information is now known.""" + LOGGER.debug( + "_async_create_entry: location: %s, UDN: %s", self._location, self._udn + ) + assert self._location + assert self._udn + assert self._device_type + + title = self._name or urlparse(self._location).hostname or DEFAULT_NAME data = { - CONF_URL: discovery[ssdp.ATTR_SSDP_LOCATION], - CONF_DEVICE_ID: discovery[ssdp.ATTR_SSDP_UDN], - CONF_TYPE: discovery.get(ssdp.ATTR_SSDP_NT) or discovery[ssdp.ATTR_SSDP_ST], + CONF_URL: self._location, + CONF_DEVICE_ID: self._udn, + CONF_TYPE: self._device_type, } - return self.async_create_entry(title=title, data=data, options=options) + return self.async_create_entry(title=title, data=data, options=self._options) + + async def _async_set_info_from_discovery( + self, discovery_info: Mapping[str, Any], abort_if_configured: bool = True + ) -> None: + """Set information required for a config entry from the SSDP discovery.""" + LOGGER.debug( + "_async_set_info_from_discovery: location: %s, UDN: %s", + discovery_info[ssdp.ATTR_SSDP_LOCATION], + discovery_info[ssdp.ATTR_SSDP_UDN], + ) + + if not self._location: + self._location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + assert isinstance(self._location, str) + + self._udn = discovery_info[ssdp.ATTR_SSDP_UDN] + await self.async_set_unique_id(self._udn) + + if abort_if_configured: + # Abort if already configured, but update the last-known location + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) + + self._device_type = ( + discovery_info.get(ssdp.ATTR_SSDP_NT) or discovery_info[ssdp.ATTR_SSDP_ST] + ) + self._name = ( + discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(self._location).hostname + or DEFAULT_NAME + ) async def _async_get_discoveries(self) -> list[Mapping[str, str]]: """Get list of unconfigured DLNA devices discovered by SSDP.""" @@ -245,32 +308,6 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return discoveries - async def _async_connect(self, location: str) -> dict[str, str]: - """Connect to a device to confirm it works and get discovery information. - - Raises ConnectError if something goes wrong. - """ - LOGGER.debug("_async_connect(location=%s)", location) - domain_data = get_domain_data(self.hass) - try: - device = await domain_data.upnp_factory.async_create_device(location) - except UpnpError as err: - raise ConnectError("could_not_connect") from err - - try: - device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) - except UpnpError as err: - raise ConnectError("not_dmr") from err - - discovery = { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_UDN: device.udn, - ssdp.ATTR_SSDP_ST: device.device_type, - ssdp.ATTR_UPNP_FRIENDLY_NAME: device.name, - } - - return discovery - class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): """Handle a DLNA DMR options flow. diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 8d4693dd435..d7b330f0fe8 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Mapping -from typing import Any, NamedTuple, cast +from typing import NamedTuple, cast from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester @@ -33,7 +32,6 @@ class DlnaDmrData: event_notifiers: dict[EventListenAddr, AiohttpNotifyServer] event_notifier_refs: defaultdict[EventListenAddr, int] stop_listener_remove: CALLBACK_TYPE | None = None - unmigrated_config: dict[str, Mapping[str, Any]] def __init__(self, hass: HomeAssistant) -> None: """Initialize global data.""" @@ -43,11 +41,9 @@ class DlnaDmrData: self.upnp_factory = UpnpFactory(self.requester, non_strict=True) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) - self.unmigrated_config = {} async def async_cleanup_event_notifiers(self, event: Event) -> None: """Clean up resources when Home Assistant is stopped.""" - del event # unused LOGGER.debug("Cleaning resources in DlnaDmrData") async with self.lock: tasks = (server.stop_server() for server in self.event_notifiers.values()) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 53bee3d8519..7c47d9329bb 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.22.5"], - "dependencies": ["network", "ssdp"], + "dependencies": ["ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 8542464e41e..839b58b6b5a 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -42,12 +42,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, - DEFAULT_NAME, DOMAIN, LOGGER as _LOGGER, MEDIA_TYPE_MAP, @@ -69,7 +69,7 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LISTEN_IP): cv.string, vol.Optional(CONF_LISTEN_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, } ), @@ -98,13 +98,35 @@ def catch_request_errors(func: Func) -> Func: return cast(Func, wrapper) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up DLNA media_player platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, + ) + ) + + _LOGGER.warning( + "Configuring dlna_dmr via yaml is deprecated; the configuration for" + " %s will be migrated to a config entry and can be safely removed when" + "migration is complete", + config.get(CONF_URL), + ) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the DlnaDmrEntity from a config entry.""" - del hass # Unused _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) # Create our own device-wrapping entity @@ -118,10 +140,6 @@ async def async_setup_entry( location=entry.data[CONF_URL], ) - entry.async_on_unload( - entry.add_update_listener(entity.async_config_update_listener) - ) - async_add_entities([entity]) @@ -139,7 +157,6 @@ class DlnaDmrEntity(MediaPlayerEntity): _device_lock: asyncio.Lock # Held when connecting or disconnecting the device _device: DmrDevice | None = None - _remove_ssdp_callbacks: list[Callable] check_available: bool = False # Track BOOTID in SSDP advertisements for device changes @@ -167,10 +184,19 @@ class DlnaDmrEntity(MediaPlayerEntity): self.poll_availability = poll_availability self.location = location self._device_lock = asyncio.Lock() - self._remove_ssdp_callbacks = [] async def async_added_to_hass(self) -> None: """Handle addition.""" + # Update this entity when the associated config entry is modified + if self.registry_entry and self.registry_entry.config_entry_id: + config_entry = self.hass.config_entries.async_get_entry( + self.registry_entry.config_entry_id + ) + assert config_entry is not None + self.async_on_remove( + config_entry.add_update_listener(self.async_config_update_listener) + ) + # Try to connect to the last known location, but don't worry if not available if not self._device: try: @@ -179,7 +205,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.debug("Couldn't connect immediately: %r", err) # Get SSDP notifications for only this device - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, {"USN": self.usn} ) @@ -189,7 +215,7 @@ class DlnaDmrEntity(MediaPlayerEntity): # (device name) which often is not the USN (service within the device) # that we're interested in. So also listen for byebye advertisements for # the UDN, which is reported in the _udn field of the combined_headers. - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, @@ -199,9 +225,6 @@ class DlnaDmrEntity(MediaPlayerEntity): async def async_will_remove_from_hass(self) -> None: """Handle removal.""" - for callback in self._remove_ssdp_callbacks: - callback() - self._remove_ssdp_callbacks.clear() await self._device_disconnect() async def async_ssdp_callback( @@ -255,13 +278,12 @@ class DlnaDmrEntity(MediaPlayerEntity): ) # Device could have been de/re-connected, state probably changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_config_update_listener( self, hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update by modifying self in-place.""" - del hass # Unused _LOGGER.debug( "Updating: %s with data=%s and options=%s", self.name, @@ -292,7 +314,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.warning("Couldn't (re)connect after config change: %r", err) # Device was de/re-connected, state might have changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def _device_connect(self, location: str) -> None: """Connect to the device now that it's available.""" @@ -415,11 +437,10 @@ class DlnaDmrEntity(MediaPlayerEntity): self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] ) -> None: """State variable(s) changed, let home-assistant know.""" - del service # Unused if not state_variables: # Indicates a failure to resubscribe, check if device is still available self.check_available = True - self.schedule_update_ha_state() + self.async_write_ha_state() @property def available(self) -> bool: diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index 27e96b465db..c418305d2e6 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -9,6 +9,9 @@ "url": "[%key:common::config_flow::data::url%]" } }, + "import_turn_on": { + "description": "Please turn on the device and click submit to continue migration" + }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" } diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 94bbd365e18..c307d6b3571 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -17,6 +17,9 @@ "confirm": { "description": "Do you want to start set up?" }, + "import_turn_on": { + "description": "Please turn on the device and click confirm to continue migration" + }, "user": { "data": { "url": "URL" diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 60116d949ae..f352349205e 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -52,20 +52,12 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: seal(upnp_device) domain_data.upnp_factory.async_create_device.return_value = upnp_device - domain_data.unmigrated_config = {} - with patch.dict(hass.data, {DLNA_DOMAIN: domain_data}): yield domain_data - # Make sure the event notifiers are released - assert ( - domain_data.async_get_event_notifier.await_count - == domain_data.async_release_event_notifier.await_count - ) - @pytest.fixture -def config_entry_mock() -> Iterable[MockConfigEntry]: +def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" mock_entry = MockConfigEntry( unique_id=MOCK_DEVICE_UDN, @@ -78,7 +70,7 @@ def config_entry_mock() -> Iterable[MockConfigEntry]: title=MOCK_DEVICE_NAME, options={}, ) - yield mock_entry + return mock_entry @pytest.fixture @@ -100,14 +92,6 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]: yield device - # Make sure the device is disconnected - assert ( - device.async_subscribe_services.await_count - == device.async_unsubscribe_services.await_count - ) - - assert device.on_event is None - @pytest.fixture(name="skip_notifications", autouse=True) def skip_notifications_fixture() -> Iterable[None]: @@ -125,9 +109,6 @@ def ssdp_scanner_mock() -> Iterable[Mock]: reg_callback = mock_scanner.return_value.async_register_callback reg_callback.return_value = Mock(return_value=None) yield mock_scanner.return_value - assert ( - reg_callback.call_count == reg_callback.return_value.call_count - ), "Not all callbacks unregistered" @pytest.fixture(autouse=True) diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 1bf93781be1..0586a43422a 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -86,12 +86,6 @@ async def test_user_flow(hass: HomeAssistant) -> None: # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_user_flow_uncontactable( hass: HomeAssistant, domain_data_mock: Mock @@ -154,12 +148,6 @@ async def test_user_flow_embedded_st( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) -> None: """Test user-init'd config flow with user entering a URL for the wrong device.""" @@ -194,30 +182,6 @@ async def test_import_flow_invalid(hass: HomeAssistant, domain_data_mock: Mock) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_config" - # Device is not contactable - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PLATFORM: DLNA_DOMAIN, CONF_URL: MOCK_DEVICE_LOCATION}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device is the wrong type - domain_data_mock.upnp_factory.async_create_device.side_effect = None - upnp_device = domain_data_mock.upnp_factory.async_create_device.return_value - upnp_device.device_type = WRONG_DEVICE_TYPE - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PLATFORM: DLNA_DOMAIN, CONF_URL: MOCK_DEVICE_LOCATION}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_dmr" - async def test_import_flow_ssdp_discovered( hass: HomeAssistant, ssdp_scanner_mock: Mock @@ -248,7 +212,6 @@ async def test_import_flow_ssdp_discovered( CONF_CALLBACK_URL_OVERRIDE: None, CONF_POLL_AVAILABILITY: False, } - entry_id = result["result"].entry_id # The config entry should not be duplicated when dlna_dmr is restarted ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ @@ -267,11 +230,6 @@ async def test_import_flow_ssdp_discovered( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_import_flow_direct_connect( hass: HomeAssistant, ssdp_scanner_mock: Mock @@ -299,7 +257,6 @@ async def test_import_flow_direct_connect( CONF_CALLBACK_URL_OVERRIDE: None, CONF_POLL_AVAILABILITY: True, } - entry_id = result["result"].entry_id # The config entry should not be duplicated when dlna_dmr is restarted result = await hass.config_entries.flow.async_init( @@ -310,10 +267,78 @@ async def test_import_flow_direct_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - # Remove the device to clean up all resources, completing its life cycle - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False + +async def test_import_flow_offline( + hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock +) -> None: + """Test import flow of offline device.""" + # Device is not yet contactable + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_PLATFORM: DLNA_DOMAIN, + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_NAME: IMPORTED_DEVICE_NAME, + CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", + CONF_LISTEN_PORT: 2222, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "import_turn_on" + + import_flow_id = result["flow_id"] + + # User clicks submit, same form is displayed with an error + result = await hass.config_entries.flow.async_configure( + import_flow_id, user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "could_not_connect"} + assert result["step_id"] == "import_turn_on" + + # Device is discovered via SSDP, new flow should not be initialized + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + domain_data_mock.upnp_factory.async_create_device.side_effect = None + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + # User clicks submit, config entry should be created + result = await hass.config_entries.flow.async_configure( + import_flow_id, user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == IMPORTED_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, } + # Options should be retained + assert result["options"] == { + CONF_LISTEN_PORT: 2222, + CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", + CONF_POLL_AVAILABILITY: True, + } + + # Wait for platform to be fully setup + await hass.async_block_till_done() async def test_import_flow_options( @@ -351,134 +376,6 @@ async def test_import_flow_options( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - - -async def test_import_flow_deferred_ssdp( - hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock -) -> None: - """Test YAML import of unavailable device later found via SSDP.""" - # Attempted import at hass start fails because device is unavailable - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [], - [], - [], - ] - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device becomes available then discovered via SSDP, import now occurs automatically - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [MOCK_DISCOVERY], - [], - [], - ] - domain_data_mock.upnp_factory.async_create_device.side_effect = None - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) - await hass.async_block_till_done() - - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_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_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: False, - } - - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - - -async def test_import_flow_deferred_user( - hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock -) -> None: - """Test YAML import of unavailable device later added by user.""" - # Attempted import at hass start fails because device is unavailable - ssdp_scanner_mock.async_get_discovery_info_by_st.return_value = [] - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device becomes available then added by user, use all imported settings - domain_data_mock.upnp_factory.async_create_device.side_effect = None - - 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" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} - ) - await hass.async_block_till_done() - - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_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_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: True, - } - - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_ssdp_flow_success(hass: HomeAssistant) -> None: """Test that SSDP discovery with an available device works.""" @@ -488,7 +385,6 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: data=MOCK_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -505,20 +401,14 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: } assert result["options"] == {} - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_ssdp_flow_unavailable( hass: HomeAssistant, domain_data_mock: Mock ) -> None: - """Test that SSDP discovery with an unavailable device gives an error message. + """Test that SSDP discovery with an unavailable device still succeeds. - This may occur if the device is turned on, discovered, then turned off - before the user attempts to add it. + All the required information for configuration is obtained from the SSDP + message, there's no need to connect to the device to configure it. """ result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, @@ -526,7 +416,6 @@ async def test_ssdp_flow_unavailable( data=MOCK_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -534,9 +423,16 @@ async def test_ssdp_flow_unavailable( 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"] == {"base": "could_not_connect"} - assert result["step_id"] == "confirm" + 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"] == {} async def test_ssdp_flow_existing( diff --git a/tests/components/dlna_dmr/test_init.py b/tests/components/dlna_dmr/test_init.py index 91aec7310ab..be793d67c5e 100644 --- a/tests/components/dlna_dmr/test_init.py +++ b/tests/components/dlna_dmr/test_init.py @@ -1,59 +1,60 @@ -"""Tests for the DLNA DMR __init__ module.""" +"""Test the DLNA DMR component setup and cleanup.""" from unittest.mock import Mock -from async_upnp_client import UpnpError - -from homeassistant.components.dlna_dmr.const import ( - CONF_LISTEN_PORT, - DOMAIN as DLNA_DOMAIN, -) -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_URL +from homeassistant.components import media_player +from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from .conftest import MOCK_DEVICE_LOCATION +from tests.common import MockConfigEntry -async def test_import_flow_started(hass: HomeAssistant, domain_data_mock: Mock) -> None: - """Test import flow of YAML config is started if there's config data.""" - mock_config: ConfigType = { - MEDIA_PLAYER_DOMAIN: [ - { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_LISTEN_PORT: 1234, - }, - { - CONF_PLATFORM: "other_domain", - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: "another device", - }, - ] - } - - # Device is not available yet - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - - # Run the setup - await async_setup_component(hass, DLNA_DOMAIN, mock_config) +async def test_resource_lifecycle( + hass: HomeAssistant, + domain_data_mock: Mock, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dmr_device_mock: Mock, +) -> None: + """Test that resources are acquired/released as the entity is setup/unloaded.""" + # Set up the config entry + config_entry_mock.add_to_hass(hass) + assert await async_setup_component(hass, DLNA_DOMAIN, {}) is True await hass.async_block_till_done() - # Check config_flow has completed - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - # Check device contact attempt was made - domain_data_mock.upnp_factory.async_create_device.assert_awaited_once_with( - MOCK_DEVICE_LOCATION + # Check the entity is created and working + entries = entity_registry.async_entries_for_config_entry( + entity_registry.async_get(hass), config_entry_mock.entry_id ) + assert len(entries) == 1 + entity_id = entries[0].entity_id + mock_state = hass.states.get(entity_id) + assert mock_state is not None + assert mock_state.state == media_player.STATE_IDLE - # Check the device is added to the unmigrated configs - assert domain_data_mock.unmigrated_config == { - MOCK_DEVICE_LOCATION: { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_LISTEN_PORT: 1234, - } + # Check update listeners and event notifiers are subscribed + assert len(config_entry_mock.update_listeners) == 1 + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is not None + + # Unload the config entry + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False } + + # Check update listeners and event notifiers are released + assert not config_entry_mock.update_listeners + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 1 + assert dmr_device_mock.on_event is None diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 4c27de1be67..2d02c8f1a8f 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -24,7 +24,7 @@ from homeassistant.components.dlna_dmr.const import ( from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.media_player import ATTR_TO_PROPERTY, const as mp_const from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_dr from homeassistant.helpers.entity_component import async_update_entity @@ -32,6 +32,7 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get as async_get_er, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from .conftest import ( @@ -65,7 +66,9 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) @pytest.fixture async def mock_entity_id( hass: HomeAssistant, + domain_data_mock: Mock, config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, dmr_device_mock: Mock, ) -> AsyncIterable[str]: """Fixture to set up a mock DlnaDmrEntity in a connected state. @@ -74,8 +77,17 @@ async def mock_entity_id( """ entity_id = await setup_mock_component(hass, config_entry_mock) + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is not None + # Run the test yield entity_id # Unload config entry to clean up @@ -83,12 +95,29 @@ async def mock_entity_id( "require_restart": False } + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + domain_data_mock.async_get_event_notifier.await_count + == domain_data_mock.async_release_event_notifier.await_count + ) + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + assert ( + dmr_device_mock.async_subscribe_services.await_count + == dmr_device_mock.async_unsubscribe_services.await_count + ) + assert dmr_device_mock.on_event is None + @pytest.fixture async def mock_disconnected_entity_id( hass: HomeAssistant, domain_data_mock: Mock, config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, dmr_device_mock: Mock, ) -> AsyncIterable[str]: """Fixture to set up a mock DlnaDmrEntity in a disconnected state. @@ -100,8 +129,19 @@ async def mock_disconnected_entity_id( entity_id = await setup_mock_component(hass, config_entry_mock) - assert dmr_device_mock.async_subscribe_services.await_count == 0 + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + # The DmrDevice hasn't been instantiated yet + assert domain_data_mock.async_get_event_notifier.await_count == 0 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert dmr_device_mock.async_subscribe_services.await_count == 0 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is None + + # Run the test yield entity_id # Unload config entry to clean up @@ -109,6 +149,54 @@ async def mock_disconnected_entity_id( "require_restart": False } + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + domain_data_mock.async_get_event_notifier.await_count + == domain_data_mock.async_release_event_notifier.await_count + ) + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + assert ( + dmr_device_mock.async_subscribe_services.await_count + == dmr_device_mock.async_unsubscribe_services.await_count + ) + assert dmr_device_mock.on_event is None + + +async def test_setup_platform_import_flow_started( + hass: HomeAssistant, domain_data_mock: Mock +) -> None: + """Test import flow of YAML config is started if there's config data.""" + # Cause connection attempts to fail + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError + + # Run the setup + mock_config: ConfigType = { + MP_DOMAIN: [ + { + CONF_PLATFORM: DLNA_DOMAIN, + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_LISTEN_PORT: 1234, + } + ] + } + + await async_setup_component(hass, MP_DOMAIN, mock_config) + await hass.async_block_till_done() + + # Check config_flow has started + flows = hass.config_entries.flow.async_progress(include_uninitialized=True) + assert len(flows) == 1 + + # It should be paused, waiting for the user to turn on the device + flow = flows[0] + assert flow["handler"] == "dlna_dmr" + assert flow["step_id"] == "import_turn_on" + assert flow["context"].get("unique_id") == MOCK_DEVICE_LOCATION + async def test_setup_entry_no_options( hass: HomeAssistant, @@ -799,7 +887,7 @@ async def test_ssdp_byebye( # Device should be gone mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == media_player.STATE_IDLE + assert mock_state.state == ha_const.STATE_UNAVAILABLE # Second byebye will do nothing await ssdp_callback(