Avoid delaying startup in dlna_dmr (#109836)

* Avoid delaying startup in dlna_dmr

fixes #109834

* make sure device info is linked up at startup

* fixes

* update tests

* startup only

* override device info if we have it

* fixes

* make sure its set right away when adding the device

* revert test changes

* coverage

* coverage

* coverage

* coverage

* adjust

* fixes

* more fixes

* coverage

* coverage

* coverage

* tweaks

* tweaks

* Revert "revert test changes"

This reverts commit 014d29297dac9dda45a50ec9eb2fa63a735538ac.

* coverage

* coverage
This commit is contained in:
J. Nick Koston 2024-02-09 08:05:27 -06:00 committed by GitHub
parent 6e134b325d
commit 8e4714c563
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 300 additions and 45 deletions

View File

@ -19,6 +19,7 @@ from homeassistant import config_entries
from homeassistant.components import media_source, ssdp from homeassistant.components import media_source, ssdp
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
ATTR_MEDIA_EXTRA, ATTR_MEDIA_EXTRA,
DOMAIN as MEDIA_PLAYER_DOMAIN,
BrowseMedia, BrowseMedia,
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
@ -28,7 +29,7 @@ from homeassistant.components.media_player import (
async_process_play_media_url, async_process_play_media_url,
) )
from homeassistant.const import CONF_DEVICE_ID, CONF_MAC, CONF_TYPE, CONF_URL from homeassistant.const import CONF_DEVICE_ID, CONF_MAC, CONF_TYPE, CONF_URL
from homeassistant.core import HomeAssistant from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -37,6 +38,7 @@ from .const import (
CONF_CALLBACK_URL_OVERRIDE, CONF_CALLBACK_URL_OVERRIDE,
CONF_LISTEN_PORT, CONF_LISTEN_PORT,
CONF_POLL_AVAILABILITY, CONF_POLL_AVAILABILITY,
DOMAIN,
LOGGER as _LOGGER, LOGGER as _LOGGER,
MEDIA_METADATA_DIDL, MEDIA_METADATA_DIDL,
MEDIA_TYPE_MAP, MEDIA_TYPE_MAP,
@ -87,9 +89,32 @@ async def async_setup_entry(
"""Set up the DlnaDmrEntity from a config entry.""" """Set up the DlnaDmrEntity from a config entry."""
_LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title)
udn = entry.data[CONF_DEVICE_ID]
ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
if (
(
existing_entity_id := ent_reg.async_get_entity_id(
domain=MEDIA_PLAYER_DOMAIN, platform=DOMAIN, unique_id=udn
)
)
and (existing_entry := ent_reg.async_get(existing_entity_id))
and (device_id := existing_entry.device_id)
and (device_entry := dev_reg.async_get(device_id))
and (dr.CONNECTION_UPNP, udn) not in device_entry.connections
):
# If the existing device is missing the udn connection, add it
# now to ensure that when the entity gets added it is linked to
# the correct device.
dev_reg.async_update_device(
device_id,
merge_connections={(dr.CONNECTION_UPNP, udn)},
)
# Create our own device-wrapping entity # Create our own device-wrapping entity
entity = DlnaDmrEntity( entity = DlnaDmrEntity(
udn=entry.data[CONF_DEVICE_ID], udn=udn,
device_type=entry.data[CONF_TYPE], device_type=entry.data[CONF_TYPE],
name=entry.title, name=entry.title,
event_port=entry.options.get(CONF_LISTEN_PORT) or 0, event_port=entry.options.get(CONF_LISTEN_PORT) or 0,
@ -98,6 +123,7 @@ async def async_setup_entry(
location=entry.data[CONF_URL], location=entry.data[CONF_URL],
mac_address=entry.data.get(CONF_MAC), mac_address=entry.data.get(CONF_MAC),
browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False), browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False),
config_entry=entry,
) )
async_add_entities([entity]) async_add_entities([entity])
@ -143,6 +169,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
location: str, location: str,
mac_address: str | None, mac_address: str | None,
browse_unfiltered: bool, browse_unfiltered: bool,
config_entry: config_entries.ConfigEntry,
) -> None: ) -> None:
"""Initialize DLNA DMR entity.""" """Initialize DLNA DMR entity."""
self.udn = udn self.udn = udn
@ -154,25 +181,17 @@ class DlnaDmrEntity(MediaPlayerEntity):
self.mac_address = mac_address self.mac_address = mac_address
self.browse_unfiltered = browse_unfiltered self.browse_unfiltered = browse_unfiltered
self._device_lock = asyncio.Lock() self._device_lock = asyncio.Lock()
self._background_setup_task: asyncio.Task[None] | None = None
self._updated_registry: bool = False
self._config_entry = config_entry
self._attr_device_info = dr.DeviceInfo(connections={(dr.CONNECTION_UPNP, udn)})
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle addition.""" """Handle addition."""
# Update this entity when the associated config entry is modified # Update this entity when the associated config entry is modified
if self.registry_entry and self.registry_entry.config_entry_id: self.async_on_remove(
config_entry = self.hass.config_entries.async_get_entry( self._config_entry.add_update_listener(self.async_config_update_listener)
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:
await self._device_connect(self.location)
except UpnpError as err:
_LOGGER.debug("Couldn't connect immediately: %r", err)
# Get SSDP notifications for only this device # Get SSDP notifications for only this device
self.async_on_remove( self.async_on_remove(
@ -193,8 +212,29 @@ class DlnaDmrEntity(MediaPlayerEntity):
) )
) )
if not self._device:
if self.hass.state is CoreState.running:
await self._async_setup()
else:
self._background_setup_task = self.hass.async_create_background_task(
self._async_setup(), f"dlna_dmr {self.name} setup"
)
async def _async_setup(self) -> None:
# Try to connect to the last known location, but don't worry if not available
try:
await self._device_connect(self.location)
except UpnpError as err:
_LOGGER.debug("Couldn't connect immediately: %r", err)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Handle removal.""" """Handle removal."""
if self._background_setup_task:
self._background_setup_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._background_setup_task
self._background_setup_task = None
await self._device_disconnect() await self._device_disconnect()
async def async_ssdp_callback( async def async_ssdp_callback(
@ -351,25 +391,28 @@ class DlnaDmrEntity(MediaPlayerEntity):
def _update_device_registry(self, set_mac: bool = False) -> None: def _update_device_registry(self, set_mac: bool = False) -> None:
"""Update the device registry with new information about the DMR.""" """Update the device registry with new information about the DMR."""
if not self._device: if (
return # Can't get all the required information without a connection # Can't get all the required information without a connection
not self._device
or
# No new information
(not set_mac and self._updated_registry)
):
return
if not self.registry_entry or not self.registry_entry.config_entry_id:
return # No config registry entry to link to
if self.registry_entry.device_id and not set_mac:
return # No new information
connections = set()
# Connections based on the root device's UDN, and the DMR embedded # Connections based on the root device's UDN, and the DMR embedded
# device's UDN. They may be the same, if the DMR is the root device. # device's UDN. They may be the same, if the DMR is the root device.
connections.add( connections = {
( (
dr.CONNECTION_UPNP, dr.CONNECTION_UPNP,
self._device.profile_device.root_device.udn, self._device.profile_device.root_device.udn,
) ),
) (dr.CONNECTION_UPNP, self._device.udn),
connections.add((dr.CONNECTION_UPNP, self._device.udn)) (
dr.CONNECTION_UPNP,
self.udn,
),
}
if self.mac_address: if self.mac_address:
# Connection based on MAC address, if known # Connection based on MAC address, if known
@ -378,23 +421,27 @@ class DlnaDmrEntity(MediaPlayerEntity):
(dr.CONNECTION_NETWORK_MAC, self.mac_address) (dr.CONNECTION_NETWORK_MAC, self.mac_address)
) )
# Create linked HA DeviceEntry now the information is known. device_info = dr.DeviceInfo(
dev_reg = dr.async_get(self.hass)
device_entry = dev_reg.async_get_or_create(
config_entry_id=self.registry_entry.config_entry_id,
connections=connections, connections=connections,
default_manufacturer=self._device.manufacturer, default_manufacturer=self._device.manufacturer,
default_model=self._device.model_name, default_model=self._device.model_name,
default_name=self._device.name, default_name=self._device.name,
) )
self._attr_device_info = device_info
self._updated_registry = True
# Create linked HA DeviceEntry now the information is known.
device_entry = dr.async_get(self.hass).async_get_or_create(
config_entry_id=self._config_entry.entry_id, **device_info
)
# Update entity registry to link to the device # Update entity registry to link to the device
ent_reg = er.async_get(self.hass) er.async_get(self.hass).async_get_or_create(
ent_reg.async_get_or_create( MEDIA_PLAYER_DOMAIN,
self.registry_entry.domain, DOMAIN,
self.registry_entry.platform,
self.unique_id, self.unique_id,
device_id=device_entry.id, device_id=device_entry.id,
config_entry=self._config_entry,
) )
async def _device_disconnect(self) -> None: async def _device_disconnect(self) -> None:
@ -419,6 +466,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Retrieve the latest data.""" """Retrieve the latest data."""
if self._background_setup_task:
await self._background_setup_task
self._background_setup_task = None
if not self._device: if not self._device:
if not self.poll_availability: if not self.poll_availability:
return return

View File

@ -6,6 +6,7 @@ from homeassistant.components import media_player
from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -31,6 +32,10 @@ async def test_resource_lifecycle(
) )
assert len(entries) == 1 assert len(entries) == 1
entity_id = entries[0].entity_id entity_id = entries[0].entity_id
await async_update_entity(hass, entity_id)
await hass.async_block_till_done()
mock_state = hass.states.get(entity_id) mock_state = hass.states.get(entity_id)
assert mock_state is not None assert mock_state is not None
assert mock_state.state == media_player.STATE_IDLE assert mock_state.state == media_player.STATE_IDLE

View File

@ -26,6 +26,7 @@ from homeassistant.components.dlna_dmr.const import (
CONF_CALLBACK_URL_OVERRIDE, CONF_CALLBACK_URL_OVERRIDE,
CONF_LISTEN_PORT, CONF_LISTEN_PORT,
CONF_POLL_AVAILABILITY, CONF_POLL_AVAILABILITY,
DOMAIN,
) )
from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.dlna_dmr.data import EventListenAddr
from homeassistant.components.dlna_dmr.media_player import DlnaDmrEntity from homeassistant.components.dlna_dmr.media_player import DlnaDmrEntity
@ -46,7 +47,7 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
CONF_URL, CONF_URL,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers.device_registry import ( from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC, CONNECTION_NETWORK_MAC,
CONNECTION_UPNP, CONNECTION_UPNP,
@ -216,6 +217,9 @@ async def test_setup_entry_no_options(
""" """
config_entry_mock.options = MappingProxyType({}) config_entry_mock.options = MappingProxyType({})
mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None assert mock_state is not None
@ -266,17 +270,23 @@ async def test_setup_entry_no_options(
assert mock_state.state == ha_const.STATE_UNAVAILABLE assert mock_state.state == ha_const.STATE_UNAVAILABLE
@pytest.mark.parametrize(
"core_state",
(CoreState.not_running, CoreState.running),
)
async def test_setup_entry_with_options( async def test_setup_entry_with_options(
hass: HomeAssistant, hass: HomeAssistant,
domain_data_mock: Mock, domain_data_mock: Mock,
ssdp_scanner_mock: Mock, ssdp_scanner_mock: Mock,
config_entry_mock: MockConfigEntry, config_entry_mock: MockConfigEntry,
dmr_device_mock: Mock, dmr_device_mock: Mock,
core_state: CoreState,
) -> None: ) -> None:
"""Test setting options leads to a DlnaDmrEntity with custom event_handler. """Test setting options leads to a DlnaDmrEntity with custom event_handler.
Check that the device is constructed properly as part of the test. Check that the device is constructed properly as part of the test.
""" """
hass.set_state(core_state)
config_entry_mock.options = MappingProxyType( config_entry_mock.options = MappingProxyType(
{ {
CONF_LISTEN_PORT: 2222, CONF_LISTEN_PORT: 2222,
@ -285,6 +295,8 @@ async def test_setup_entry_with_options(
} }
) )
mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None assert mock_state is not None
@ -343,8 +355,9 @@ async def test_setup_entry_mac_address(
dmr_device_mock: Mock, dmr_device_mock: Mock,
) -> None: ) -> None:
"""Entry with a MAC address will set up and set the device registry connection.""" """Entry with a MAC address will set up and set the device registry connection."""
await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
# Check the device registry connections for MAC address # Check the device registry connections for MAC address
dev_reg = async_get_dr(hass) dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device( device = dev_reg.async_get_device(
@ -363,8 +376,9 @@ async def test_setup_entry_no_mac_address(
dmr_device_mock: Mock, dmr_device_mock: Mock,
) -> None: ) -> None:
"""Test setting up an entry without a MAC address will succeed.""" """Test setting up an entry without a MAC address will succeed."""
await setup_mock_component(hass, config_entry_mock_no_mac) mock_entity_id = await setup_mock_component(hass, config_entry_mock_no_mac)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
# Check the device registry connections does not include the MAC address # Check the device registry connections does not include the MAC address
dev_reg = async_get_dr(hass) dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device( device = dev_reg.async_get_device(
@ -382,6 +396,8 @@ async def test_event_subscribe_failure(
dmr_device_mock.async_subscribe_services.side_effect = UpnpError dmr_device_mock.async_subscribe_services.side_effect = UpnpError
mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None assert mock_state is not None
@ -412,6 +428,8 @@ async def test_event_subscribe_rejected(
dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501) dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(status=501)
mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None assert mock_state is not None
@ -432,6 +450,8 @@ async def test_available_device(
) -> None: ) -> None:
"""Test a DlnaDmrEntity with a connected DmrDevice.""" """Test a DlnaDmrEntity with a connected DmrDevice."""
# Check hass device information is filled in # Check hass device information is filled in
await async_update_entity(hass, mock_entity_id)
await hass.async_block_till_done()
dev_reg = async_get_dr(hass) dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device( device = dev_reg.async_get_device(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
@ -1235,14 +1255,20 @@ async def test_playback_update_state(
dmr_device_mock.async_update.assert_not_awaited() dmr_device_mock.async_update.assert_not_awaited()
@pytest.mark.parametrize(
"core_state",
(CoreState.not_running, CoreState.running),
)
async def test_unavailable_device( async def test_unavailable_device(
hass: HomeAssistant, hass: HomeAssistant,
domain_data_mock: Mock, domain_data_mock: Mock,
ssdp_scanner_mock: Mock, ssdp_scanner_mock: Mock,
config_entry_mock: MockConfigEntry, config_entry_mock: MockConfigEntry,
core_state: CoreState,
) -> None: ) -> None:
"""Test a DlnaDmrEntity with out a connected DmrDevice.""" """Test a DlnaDmrEntity with out a connected DmrDevice."""
# Cause connection attempts to fail # Cause connection attempts to fail
hass.set_state(core_state)
domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError
with patch( with patch(
@ -1336,7 +1362,9 @@ async def test_unavailable_device(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
identifiers=set(), identifiers=set(),
) )
assert device is None assert device is not None
assert device.name is None
assert device.manufacturer is None
# Unload config entry to clean up # Unload config entry to clean up
assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == {
@ -1355,15 +1383,21 @@ async def test_unavailable_device(
assert mock_state.state == ha_const.STATE_UNAVAILABLE assert mock_state.state == ha_const.STATE_UNAVAILABLE
@pytest.mark.parametrize(
"core_state",
(CoreState.not_running, CoreState.running),
)
async def test_become_available( async def test_become_available(
hass: HomeAssistant, hass: HomeAssistant,
domain_data_mock: Mock, domain_data_mock: Mock,
ssdp_scanner_mock: Mock, ssdp_scanner_mock: Mock,
config_entry_mock: MockConfigEntry, config_entry_mock: MockConfigEntry,
dmr_device_mock: Mock, dmr_device_mock: Mock,
core_state: CoreState,
) -> None: ) -> None:
"""Test a device becoming available after the entity is constructed.""" """Test a device becoming available after the entity is constructed."""
# Cause connection attempts to fail before adding entity # Cause connection attempts to fail before adding entity
hass.set_state(core_state)
domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError
mock_entity_id = await setup_mock_component(hass, config_entry_mock) mock_entity_id = await setup_mock_component(hass, config_entry_mock)
mock_state = hass.states.get(mock_entity_id) mock_state = hass.states.get(mock_entity_id)
@ -1376,7 +1410,7 @@ async def test_become_available(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)}, connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
identifiers=set(), identifiers=set(),
) )
assert device is None assert device is not None
# Mock device is now available. # Mock device is now available.
domain_data_mock.upnp_factory.async_create_device.side_effect = None domain_data_mock.upnp_factory.async_create_device.side_effect = None
@ -1440,13 +1474,19 @@ async def test_become_available(
assert mock_state.state == ha_const.STATE_UNAVAILABLE assert mock_state.state == ha_const.STATE_UNAVAILABLE
@pytest.mark.parametrize(
"core_state",
(CoreState.not_running, CoreState.running),
)
async def test_alive_but_gone( async def test_alive_but_gone(
hass: HomeAssistant, hass: HomeAssistant,
domain_data_mock: Mock, domain_data_mock: Mock,
ssdp_scanner_mock: Mock, ssdp_scanner_mock: Mock,
mock_disconnected_entity_id: str, mock_disconnected_entity_id: str,
core_state: CoreState,
) -> None: ) -> None:
"""Test a device sending an SSDP alive announcement, but not being connectable.""" """Test a device sending an SSDP alive announcement, but not being connectable."""
hass.set_state(core_state)
domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError
# Send an SSDP notification from the still missing device # Send an SSDP notification from the still missing device
@ -2275,3 +2315,162 @@ async def test_config_update_mac_address(
) )
assert device is not None assert device is not None
assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections
@pytest.mark.parametrize(
"core_state",
(CoreState.not_running, CoreState.running),
)
async def test_connections_restored(
hass: HomeAssistant,
domain_data_mock: Mock,
ssdp_scanner_mock: Mock,
config_entry_mock: MockConfigEntry,
dmr_device_mock: Mock,
core_state: CoreState,
) -> None:
"""Test previous connections restored."""
# Cause connection attempts to fail before adding entity
hass.set_state(core_state)
domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError
mock_entity_id = await setup_mock_component(hass, config_entry_mock)
mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None
assert mock_state.state == ha_const.STATE_UNAVAILABLE
# Check hass device information has not been filled in yet
dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
identifiers=set(),
)
assert device is not None
# Mock device is now available.
domain_data_mock.upnp_factory.async_create_device.side_effect = None
domain_data_mock.upnp_factory.async_create_device.reset_mock()
# Send an SSDP notification from the now alive device
ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0]
await ssdp_callback(
ssdp.SsdpServiceInfo(
ssdp_usn=MOCK_DEVICE_USN,
ssdp_location=NEW_DEVICE_LOCATION,
ssdp_st=MOCK_DEVICE_TYPE,
upnp={},
),
ssdp.SsdpChange.ALIVE,
)
await hass.async_block_till_done()
# Check device was created from the supplied URL
domain_data_mock.upnp_factory.async_create_device.assert_awaited_once_with(
NEW_DEVICE_LOCATION
)
# Check event notifiers are acquired
domain_data_mock.async_get_event_notifier.assert_awaited_once_with(
EventListenAddr(LOCAL_IP, 0, None), hass
)
# Check UPnP services are subscribed
dmr_device_mock.async_subscribe_services.assert_awaited_once_with(
auto_resubscribe=True
)
assert dmr_device_mock.on_event is not None
# Quick check of the state to verify the entity has a connected DmrDevice
mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None
assert mock_state.state == MediaPlayerState.IDLE
# Check hass device information is now filled in
dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
identifiers=set(),
)
assert device is not None
previous_connections = device.connections
assert device.manufacturer == "device_manufacturer"
assert device.model == "device_model_name"
assert device.name == "device_name"
# Reload the config entry
assert await hass.config_entries.async_reload(config_entry_mock.entry_id)
await async_update_entity(hass, mock_entity_id)
# Confirm SSDP notifications unregistered
assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2
# Confirm the entity has disconnected from the device
domain_data_mock.async_release_event_notifier.assert_awaited_once()
dmr_device_mock.async_unsubscribe_services.assert_awaited_once()
# Check hass device information has not been filled in yet
dev_reg = async_get_dr(hass)
device = dev_reg.async_get_device(
connections={(CONNECTION_UPNP, MOCK_DEVICE_UDN)},
identifiers=set(),
)
assert device is not None
assert device.connections == previous_connections
# Verify the entity remains linked to the device
ent_reg = async_get_er(hass)
entry = ent_reg.async_get(mock_entity_id)
assert entry is not None
assert entry.device_id == device.id
# Verify the entity has an idle state
mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None
assert mock_state.state == MediaPlayerState.IDLE
# Unload config entry to clean up
assert await hass.config_entries.async_unload(config_entry_mock.entry_id)
async def test_udn_upnp_connection_added_if_missing(
hass: HomeAssistant,
domain_data_mock: Mock,
ssdp_scanner_mock: Mock,
config_entry_mock: MockConfigEntry,
dmr_device_mock: Mock,
) -> None:
"""Test missing upnp connection added.
We did not always add the upnp connection to the device registry, so we need to
check that it is added if missing as otherwise we might end up creating a new
device entry.
"""
config_entry_mock.add_to_hass(hass)
# Cause connection attempts to fail before adding entity
ent_reg = async_get_er(hass)
entry = ent_reg.async_get_or_create(
MP_DOMAIN,
DOMAIN,
MOCK_DEVICE_UDN,
config_entry=config_entry_mock,
)
mock_entity_id = entry.entity_id
dev_reg = async_get_dr(hass)
device = dev_reg.async_get_or_create(
config_entry_id=config_entry_mock.entry_id,
connections={(CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS)},
identifiers=set(),
)
ent_reg.async_update_entity(mock_entity_id, device_id=device.id)
domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError
assert await hass.config_entries.async_setup(config_entry_mock.entry_id) is True
await hass.async_block_till_done()
mock_state = hass.states.get(mock_entity_id)
assert mock_state is not None
assert mock_state.state == ha_const.STATE_UNAVAILABLE
# Check hass device information has not been filled in yet
dev_reg = async_get_dr(hass)
device = dev_reg.async_get(device.id)
assert device is not None
assert (CONNECTION_UPNP, MOCK_DEVICE_UDN) in device.connections