mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Match config entries for dlna_dmr based on device MAC, in addition to UDN (#74619)
* Factor out _is_dmr_device function * Use DMR device's MAC to match existing config entries Some DMR devices change their every time they boot, against the DMR specs. Try to match such devices to existing config entries by using their MAC addresses. * Add DMR device's MAC as a device_registry connection * Use doc-only IPs (RFC5737) for dlna_dmr tests
This commit is contained in:
@ -2,6 +2,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from functools import partial
|
||||
from ipaddress import IPv6Address, ip_address
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from typing import Any, Optional, cast
|
||||
@ -10,14 +12,16 @@ from urllib.parse import urlparse
|
||||
from async_upnp_client.client import UpnpError
|
||||
from async_upnp_client.profiles.dlna import DmrDevice
|
||||
from async_upnp_client.profiles.profile import find_device_of_type
|
||||
from getmac import get_mac_address
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import ssdp
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_TYPE, CONF_URL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
from homeassistant.helpers import device_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
@ -56,6 +60,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._udn: str | None = None
|
||||
self._device_type: str | None = None
|
||||
self._name: str | None = None
|
||||
self._mac: str | None = None
|
||||
self._options: dict[str, Any] = {}
|
||||
|
||||
@staticmethod
|
||||
@ -130,32 +135,56 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="alternative_integration")
|
||||
|
||||
# Abort if the device doesn't support all services required for a DmrDevice.
|
||||
# Use the discovery_info instead of DmrDevice.is_profile_device to avoid
|
||||
# contacting the device again.
|
||||
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
|
||||
if not discovery_service_list:
|
||||
if not _is_dmr_device(discovery_info):
|
||||
return self.async_abort(reason="not_dmr")
|
||||
|
||||
services = discovery_service_list.get("service")
|
||||
if not services:
|
||||
discovery_service_ids: set[str] = set()
|
||||
elif isinstance(services, list):
|
||||
discovery_service_ids = {service.get("serviceId") for service in services}
|
||||
else:
|
||||
# Only one service defined (etree_to_dict failed to make a list)
|
||||
discovery_service_ids = {services.get("serviceId")}
|
||||
|
||||
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
|
||||
return self.async_abort(reason="not_dmr")
|
||||
|
||||
# Abort if another config entry has the same location, in case the
|
||||
# device doesn't have a static and unique UDN (breaking the UPnP spec).
|
||||
self._async_abort_entries_match({CONF_URL: self._location})
|
||||
# Abort if another config entry has the same location or MAC address, in
|
||||
# case the device doesn't have a static and unique UDN (breaking the
|
||||
# UPnP spec).
|
||||
for entry in self._async_current_entries(include_ignore=True):
|
||||
if self._location == entry.data[CONF_URL]:
|
||||
return self.async_abort(reason="already_configured")
|
||||
if self._mac and self._mac == entry.data.get(CONF_MAC):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._name}
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_ignore(self, user_input: Mapping[str, Any]) -> FlowResult:
|
||||
"""Ignore this config flow, and add MAC address as secondary identifier.
|
||||
|
||||
Not all DMR devices correctly implement the spec, so their UDN may
|
||||
change between boots. Use the MAC address as a secondary identifier so
|
||||
they can still be ignored in this case.
|
||||
"""
|
||||
LOGGER.debug("async_step_ignore: user_input: %s", user_input)
|
||||
self._udn = user_input["unique_id"]
|
||||
assert self._udn
|
||||
await self.async_set_unique_id(self._udn, raise_on_progress=False)
|
||||
|
||||
# Try to get relevant info from SSDP discovery, but don't worry if it's
|
||||
# not available - the data values will just be None in that case
|
||||
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:
|
||||
await self._async_set_info_from_discovery(
|
||||
discovery, abort_if_configured=False
|
||||
)
|
||||
break
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input["title"],
|
||||
data={
|
||||
CONF_URL: self._location,
|
||||
CONF_DEVICE_ID: self._udn,
|
||||
CONF_TYPE: self._device_type,
|
||||
CONF_MAC: self._mac,
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
@ -224,6 +253,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if not self._name:
|
||||
self._name = device.name
|
||||
|
||||
if not self._mac and (host := urlparse(self._location).hostname):
|
||||
self._mac = await _async_get_mac_address(self.hass, host)
|
||||
|
||||
def _create_entry(self) -> FlowResult:
|
||||
"""Create a config entry, assuming all required information is now known."""
|
||||
LOGGER.debug(
|
||||
@ -238,6 +270,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_URL: self._location,
|
||||
CONF_DEVICE_ID: self._udn,
|
||||
CONF_TYPE: self._device_type,
|
||||
CONF_MAC: self._mac,
|
||||
}
|
||||
return self.async_create_entry(title=title, data=data, options=self._options)
|
||||
|
||||
@ -256,13 +289,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert isinstance(self._location, str)
|
||||
|
||||
self._udn = discovery_info.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
|
||||
)
|
||||
await self.async_set_unique_id(self._udn, raise_on_progress=abort_if_configured)
|
||||
|
||||
self._device_type = discovery_info.ssdp_nt or discovery_info.ssdp_st
|
||||
self._name = (
|
||||
@ -271,6 +298,17 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
or DEFAULT_NAME
|
||||
)
|
||||
|
||||
if host := discovery_info.ssdp_headers.get("_host"):
|
||||
self._mac = await _async_get_mac_address(self.hass, host)
|
||||
|
||||
if abort_if_configured:
|
||||
# Abort if already configured, but update the last-known location
|
||||
updates = {CONF_URL: self._location}
|
||||
# Set the MAC address for older entries
|
||||
if self._mac:
|
||||
updates[CONF_MAC] = self._mac
|
||||
self._abort_if_unique_id_configured(updates=updates, reload_on_update=False)
|
||||
|
||||
async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]:
|
||||
"""Get list of unconfigured DLNA devices discovered by SSDP."""
|
||||
LOGGER.debug("_get_discoveries")
|
||||
@ -408,3 +446,59 @@ def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_dmr_device(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
"""Determine if discovery is a complete DLNA DMR device.
|
||||
|
||||
Use the discovery_info instead of DmrDevice.is_profile_device to avoid
|
||||
contacting the device again.
|
||||
"""
|
||||
# Abort if the device doesn't support all services required for a DmrDevice.
|
||||
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
|
||||
if not discovery_service_list:
|
||||
return False
|
||||
|
||||
services = discovery_service_list.get("service")
|
||||
if not services:
|
||||
discovery_service_ids: set[str] = set()
|
||||
elif isinstance(services, list):
|
||||
discovery_service_ids = {service.get("serviceId") for service in services}
|
||||
else:
|
||||
# Only one service defined (etree_to_dict failed to make a list)
|
||||
discovery_service_ids = {services.get("serviceId")}
|
||||
|
||||
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_get_mac_address(hass: HomeAssistant, host: str) -> str | None:
|
||||
"""Get mac address from host name, IPv4 address, or IPv6 address."""
|
||||
# Help mypy, which has trouble with the async_add_executor_job + partial call
|
||||
mac_address: str | None
|
||||
# getmac has trouble using IPv6 addresses as the "hostname" parameter so
|
||||
# assume host is an IP address, then handle the case it's not.
|
||||
try:
|
||||
ip_addr = ip_address(host)
|
||||
except ValueError:
|
||||
mac_address = await hass.async_add_executor_job(
|
||||
partial(get_mac_address, hostname=host)
|
||||
)
|
||||
else:
|
||||
if ip_addr.version == 4:
|
||||
mac_address = await hass.async_add_executor_job(
|
||||
partial(get_mac_address, ip=host)
|
||||
)
|
||||
else:
|
||||
# Drop scope_id from IPv6 address by converting via int
|
||||
ip_addr = IPv6Address(int(ip_addr))
|
||||
mac_address = await hass.async_add_executor_job(
|
||||
partial(get_mac_address, ip6=str(ip_addr))
|
||||
)
|
||||
|
||||
if not mac_address:
|
||||
return None
|
||||
|
||||
return device_registry.format_mac(mac_address)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Renderer",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"requirements": ["async-upnp-client==0.32.3"],
|
||||
"requirements": ["async-upnp-client==0.32.3", "getmac==0.8.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
@ -28,7 +28,7 @@ from homeassistant.components.media_player import (
|
||||
RepeatMode,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE, CONF_URL
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_MAC, CONF_TYPE, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -98,6 +98,7 @@ async def async_setup_entry(
|
||||
event_callback_url=entry.options.get(CONF_CALLBACK_URL_OVERRIDE),
|
||||
poll_availability=entry.options.get(CONF_POLL_AVAILABILITY, False),
|
||||
location=entry.data[CONF_URL],
|
||||
mac_address=entry.data.get(CONF_MAC),
|
||||
browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False),
|
||||
)
|
||||
|
||||
@ -139,6 +140,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
event_callback_url: str | None,
|
||||
poll_availability: bool,
|
||||
location: str,
|
||||
mac_address: str | None,
|
||||
browse_unfiltered: bool,
|
||||
) -> None:
|
||||
"""Initialize DLNA DMR entity."""
|
||||
@ -148,6 +150,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
self._event_addr = EventListenAddr(None, event_port, event_callback_url)
|
||||
self.poll_availability = poll_availability
|
||||
self.location = location
|
||||
self.mac_address = mac_address
|
||||
self.browse_unfiltered = browse_unfiltered
|
||||
self._device_lock = asyncio.Lock()
|
||||
|
||||
@ -272,6 +275,11 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
self.poll_availability = entry.options.get(CONF_POLL_AVAILABILITY, False)
|
||||
self.browse_unfiltered = entry.options.get(CONF_BROWSE_UNFILTERED, False)
|
||||
|
||||
new_mac_address = entry.data.get(CONF_MAC)
|
||||
if new_mac_address != self.mac_address:
|
||||
self.mac_address = new_mac_address
|
||||
self._update_device_registry(set_mac=True)
|
||||
|
||||
new_port = entry.options.get(CONF_LISTEN_PORT) or 0
|
||||
new_callback_url = entry.options.get(CONF_CALLBACK_URL_OVERRIDE)
|
||||
|
||||
@ -338,27 +346,42 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
_LOGGER.debug("Error while subscribing during device connect: %r", err)
|
||||
raise
|
||||
|
||||
if (
|
||||
not self.registry_entry
|
||||
or not self.registry_entry.config_entry_id
|
||||
or self.registry_entry.device_id
|
||||
):
|
||||
return
|
||||
self._update_device_registry()
|
||||
|
||||
def _update_device_registry(self, set_mac: bool = False) -> None:
|
||||
"""Update the device registry with new information about the DMR."""
|
||||
if not self._device:
|
||||
return # Can't get all the required information without a connection
|
||||
|
||||
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
|
||||
# device's UDN. They may be the same, if the DMR is the root device.
|
||||
connections.add(
|
||||
(
|
||||
device_registry.CONNECTION_UPNP,
|
||||
self._device.profile_device.root_device.udn,
|
||||
)
|
||||
)
|
||||
connections.add((device_registry.CONNECTION_UPNP, self._device.udn))
|
||||
|
||||
if self.mac_address:
|
||||
# Connection based on MAC address, if known
|
||||
connections.add(
|
||||
# Device MAC is obtained from the config entry, which uses getmac
|
||||
(device_registry.CONNECTION_NETWORK_MAC, self.mac_address)
|
||||
)
|
||||
|
||||
# Create linked HA DeviceEntry now the information is known.
|
||||
dev_reg = device_registry.async_get(self.hass)
|
||||
device_entry = dev_reg.async_get_or_create(
|
||||
config_entry_id=self.registry_entry.config_entry_id,
|
||||
# Connections are 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.
|
||||
connections={
|
||||
(
|
||||
device_registry.CONNECTION_UPNP,
|
||||
self._device.profile_device.root_device.udn,
|
||||
),
|
||||
(device_registry.CONNECTION_UPNP, self._device.udn),
|
||||
},
|
||||
connections=connections,
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
default_manufacturer=self._device.manufacturer,
|
||||
default_model=self._device.model_name,
|
||||
|
@ -761,6 +761,7 @@ georss_ign_sismologia_client==0.3
|
||||
# homeassistant.components.qld_bushfire
|
||||
georss_qld_bushfire_alert_client==0.5
|
||||
|
||||
# homeassistant.components.dlna_dmr
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.minecraft_server
|
||||
# homeassistant.components.nmap_tracker
|
||||
|
@ -574,6 +574,7 @@ georss_ign_sismologia_client==0.3
|
||||
# homeassistant.components.qld_bushfire
|
||||
georss_qld_bushfire_alert_client==0.5
|
||||
|
||||
# homeassistant.components.dlna_dmr
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.minecraft_server
|
||||
# homeassistant.components.nmap_tracker
|
||||
|
@ -11,22 +11,23 @@ import pytest
|
||||
|
||||
from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN
|
||||
from homeassistant.components.dlna_dmr.data import DlnaDmrData
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE, CONF_URL
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_MAC, CONF_TYPE, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_DEVICE_BASE_URL = "http://192.88.99.4"
|
||||
MOCK_DEVICE_LOCATION = MOCK_DEVICE_BASE_URL + "/dmr_description.xml"
|
||||
MOCK_DEVICE_HOST_ADDR = "198.51.100.4"
|
||||
MOCK_DEVICE_LOCATION = f"http://{MOCK_DEVICE_HOST_ADDR}/dmr_description.xml"
|
||||
MOCK_DEVICE_NAME = "Test Renderer Device"
|
||||
MOCK_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
MOCK_DEVICE_UDN = "uuid:7cc6da13-7f5d-4ace-9729-58b275c52f1e"
|
||||
MOCK_DEVICE_USN = f"{MOCK_DEVICE_UDN}::{MOCK_DEVICE_TYPE}"
|
||||
MOCK_MAC_ADDRESS = "ab:cd:ef:01:02:03"
|
||||
|
||||
LOCAL_IP = "192.88.99.1"
|
||||
EVENT_CALLBACK_URL = "http://192.88.99.1/notify"
|
||||
LOCAL_IP = "198.51.100.1"
|
||||
EVENT_CALLBACK_URL = "http://198.51.100.1/notify"
|
||||
|
||||
NEW_DEVICE_LOCATION = "http://192.88.99.7" + "/dmr_description.xml"
|
||||
NEW_DEVICE_LOCATION = "http://198.51.100.7" + "/dmr_description.xml"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -80,6 +81,24 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]:
|
||||
@pytest.fixture
|
||||
def config_entry_mock() -> MockConfigEntry:
|
||||
"""Mock a config entry for this platform."""
|
||||
mock_entry = MockConfigEntry(
|
||||
unique_id=MOCK_DEVICE_UDN,
|
||||
domain=DLNA_DOMAIN,
|
||||
data={
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
},
|
||||
title=MOCK_DEVICE_NAME,
|
||||
options={},
|
||||
)
|
||||
return mock_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry_mock_no_mac() -> MockConfigEntry:
|
||||
"""Mock a config entry that does not already contain a MAC address."""
|
||||
mock_entry = MockConfigEntry(
|
||||
unique_id=MOCK_DEVICE_UDN,
|
||||
domain=DLNA_DOMAIN,
|
||||
@ -105,7 +124,7 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]:
|
||||
device.profile_device = (
|
||||
domain_data_mock.upnp_factory.async_create_device.return_value
|
||||
)
|
||||
device.media_image_url = "http://192.88.99.20:8200/AlbumArt/2624-17620.jpg"
|
||||
device.media_image_url = "http://198.51.100.20:8200/AlbumArt/2624-17620.jpg"
|
||||
device.udn = "device_udn"
|
||||
device.manufacturer = "device_manufacturer"
|
||||
device.model_name = "device_model_name"
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Test the DLNA config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
import dataclasses
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from async_upnp_client.client import UpnpDevice
|
||||
from async_upnp_client.exceptions import UpnpError
|
||||
@ -17,14 +18,16 @@ from homeassistant.components.dlna_dmr.const import (
|
||||
CONF_POLL_AVAILABILITY,
|
||||
DOMAIN as DLNA_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_TYPE, CONF_URL
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import (
|
||||
MOCK_DEVICE_HOST_ADDR,
|
||||
MOCK_DEVICE_LOCATION,
|
||||
MOCK_DEVICE_NAME,
|
||||
MOCK_DEVICE_TYPE,
|
||||
MOCK_DEVICE_UDN,
|
||||
MOCK_MAC_ADDRESS,
|
||||
NEW_DEVICE_LOCATION,
|
||||
)
|
||||
|
||||
@ -37,6 +40,8 @@ pytestmark = [
|
||||
]
|
||||
|
||||
WRONG_DEVICE_TYPE = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
||||
CHANGED_DEVICE_LOCATION = "http://198.51.100.55/dmr_description.xml"
|
||||
CHANGED_DEVICE_UDN = "uuid:7cc6da13-7f5d-4ace-9729-badbadbadbad"
|
||||
|
||||
MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE"
|
||||
|
||||
@ -45,6 +50,7 @@ MOCK_DISCOVERY = ssdp.SsdpServiceInfo(
|
||||
ssdp_location=MOCK_DEVICE_LOCATION,
|
||||
ssdp_udn=MOCK_DEVICE_UDN,
|
||||
ssdp_st=MOCK_DEVICE_TYPE,
|
||||
ssdp_headers={"_host": MOCK_DEVICE_HOST_ADDR},
|
||||
upnp={
|
||||
ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN,
|
||||
ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE,
|
||||
@ -79,6 +85,16 @@ MOCK_DISCOVERY = ssdp.SsdpServiceInfo(
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_get_mac_address() -> Iterable[Mock]:
|
||||
"""Mock the get_mac_address function to prevent network access and assist tests."""
|
||||
with patch(
|
||||
"homeassistant.components.dlna_dmr.config_flow.get_mac_address", autospec=True
|
||||
) as gma_mock:
|
||||
gma_mock.return_value = MOCK_MAC_ADDRESS
|
||||
yield gma_mock
|
||||
|
||||
|
||||
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(
|
||||
@ -98,6 +114,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None:
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {CONF_POLL_AVAILABILITY: True}
|
||||
|
||||
@ -140,6 +157,7 @@ async def test_user_flow_discovered_manual(
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {CONF_POLL_AVAILABILITY: True}
|
||||
|
||||
@ -172,6 +190,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock)
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {}
|
||||
|
||||
@ -235,6 +254,7 @@ async def test_user_flow_embedded_st(
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {CONF_POLL_AVAILABILITY: True}
|
||||
|
||||
@ -285,6 +305,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None:
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {}
|
||||
|
||||
@ -318,6 +339,7 @@ async def test_ssdp_flow_unavailable(
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {}
|
||||
|
||||
@ -348,10 +370,80 @@ async def test_ssdp_flow_existing(
|
||||
|
||||
|
||||
async def test_ssdp_flow_duplicate_location(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry, mock_get_mac_address: Mock
|
||||
) -> None:
|
||||
"""Test that discovery of device with URL matching existing entry gets aborted."""
|
||||
# Prevent matching based on MAC address
|
||||
mock_get_mac_address.return_value = None
|
||||
config_entry_mock.add_to_hass(hass)
|
||||
|
||||
# New discovery with different UDN but same location
|
||||
discovery = dataclasses.replace(MOCK_DISCOVERY, ssdp_udn=CHANGED_DEVICE_UDN)
|
||||
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"] == "already_configured"
|
||||
assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION
|
||||
|
||||
|
||||
async def test_ssdp_duplicate_mac_ignored_entry(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test SSDP with different UDN but matching MAC for ignored config entry is ignored."""
|
||||
# Add an ignored entry
|
||||
config_entry_mock.source = config_entries.SOURCE_IGNORE
|
||||
config_entry_mock.add_to_hass(hass)
|
||||
|
||||
# Prevent matching based on location or UDN
|
||||
discovery = dataclasses.replace(
|
||||
MOCK_DISCOVERY,
|
||||
ssdp_location=CHANGED_DEVICE_LOCATION,
|
||||
ssdp_udn=CHANGED_DEVICE_UDN,
|
||||
)
|
||||
|
||||
# SSDP discovery should be aborted
|
||||
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"] == "already_configured"
|
||||
|
||||
|
||||
async def test_ssdp_duplicate_mac_configured_entry(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test SSDP with different UDN but matching MAC for existing entry is ignored."""
|
||||
config_entry_mock.add_to_hass(hass)
|
||||
|
||||
# Prevent matching based on location or UDN
|
||||
discovery = dataclasses.replace(
|
||||
MOCK_DISCOVERY,
|
||||
ssdp_location=CHANGED_DEVICE_LOCATION,
|
||||
ssdp_udn=CHANGED_DEVICE_UDN,
|
||||
)
|
||||
|
||||
# SSDP discovery should be aborted
|
||||
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"] == "already_configured"
|
||||
|
||||
|
||||
async def test_ssdp_add_mac(
|
||||
hass: HomeAssistant, config_entry_mock_no_mac: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test adding of MAC to existing entry that didn't have one."""
|
||||
config_entry_mock_no_mac.add_to_hass(hass)
|
||||
|
||||
# Start a discovery that adds the MAC address (due to auto-use mock_get_mac_address)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DLNA_DOMAIN,
|
||||
context={"source": config_entries.SOURCE_SSDP},
|
||||
@ -359,7 +451,31 @@ async def test_ssdp_flow_duplicate_location(
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Config entry should be updated to have a MAC address
|
||||
assert config_entry_mock_no_mac.data[CONF_MAC] == MOCK_MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_ssdp_dont_remove_mac(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry
|
||||
) -> None:
|
||||
"""SSDP with failure to resolve MAC should not remove MAC from config entry."""
|
||||
config_entry_mock.add_to_hass(hass)
|
||||
|
||||
# Start a discovery that fails when resolving the MAC
|
||||
mock_get_mac_address.return_value = 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_configured"
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Config entry should still have a MAC address
|
||||
assert config_entry_mock.data[CONF_MAC] == MOCK_MAC_ADDRESS
|
||||
|
||||
|
||||
async def test_ssdp_flow_upnp_udn(
|
||||
@ -497,9 +613,62 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None:
|
||||
assert result["reason"] == "alternative_integration"
|
||||
|
||||
|
||||
async def test_ignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None:
|
||||
"""Test ignoring an SSDP discovery fills in config entry data from SSDP."""
|
||||
# Device 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,
|
||||
]
|
||||
|
||||
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"] == {
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
|
||||
|
||||
async def test_ignore_flow_no_ssdp(
|
||||
hass: HomeAssistant, ssdp_scanner_mock: Mock
|
||||
) -> None:
|
||||
"""Test ignoring a flow without SSDP info still creates a config entry."""
|
||||
# Nothing found from SSDP
|
||||
ssdp_scanner_mock.async_get_discovery_info_by_udn_st.return_value = None
|
||||
|
||||
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"] == {
|
||||
CONF_URL: None,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: None,
|
||||
CONF_MAC: None,
|
||||
}
|
||||
|
||||
|
||||
async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None:
|
||||
"""Test a config flow started by unignoring a device."""
|
||||
# Create ignored entry
|
||||
# Create ignored entry (with no extra info from SSDP)
|
||||
ssdp_scanner_mock.async_get_discovery_info_by_udn_st.return_value = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DLNA_DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
@ -509,7 +678,6 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.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 = [
|
||||
@ -540,6 +708,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {}
|
||||
|
||||
@ -551,7 +720,8 @@ 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
|
||||
# Create ignored entry (with no extra info from SSDP)
|
||||
ssdp_scanner_mock.async_get_discovery_info_by_udn_st.return_value = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DLNA_DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IGNORE},
|
||||
@ -561,7 +731,6 @@ async def test_unignore_flow_offline(
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.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
|
||||
@ -576,6 +745,74 @@ async def test_unignore_flow_offline(
|
||||
assert result["reason"] == "discovery_error"
|
||||
|
||||
|
||||
async def test_get_mac_address_ipv4(
|
||||
hass: HomeAssistant, mock_get_mac_address: Mock
|
||||
) -> None:
|
||||
"""Test getting MAC address from IPv4 address for SSDP discovery."""
|
||||
# Init'ing the flow should be enough to get the MAC address
|
||||
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_FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
mock_get_mac_address.assert_called_once_with(ip=MOCK_DEVICE_HOST_ADDR)
|
||||
|
||||
|
||||
async def test_get_mac_address_ipv6(
|
||||
hass: HomeAssistant, mock_get_mac_address: Mock
|
||||
) -> None:
|
||||
"""Test getting MAC address from IPv6 address for SSDP discovery."""
|
||||
# Use a scoped link-local IPv6 address for the host
|
||||
IPV6_HOST_UNSCOPED = "fe80::1ff:fe23:4567:890a"
|
||||
IPV6_HOST = f"{IPV6_HOST_UNSCOPED}%eth2"
|
||||
IPV6_DEVICE_LOCATION = f"http://{IPV6_HOST}/dmr_description.xml"
|
||||
discovery = dataclasses.replace(MOCK_DISCOVERY, ssdp_location=IPV6_DEVICE_LOCATION)
|
||||
discovery.ssdp_headers = dict(discovery.ssdp_headers)
|
||||
discovery.ssdp_headers["_host"] = IPV6_HOST
|
||||
|
||||
# Init'ing the flow should be enough to get the MAC address
|
||||
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_FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
# The scope must be removed for get_mac_address to work correctly
|
||||
mock_get_mac_address.assert_called_once_with(ip6=IPV6_HOST_UNSCOPED)
|
||||
|
||||
|
||||
async def test_get_mac_address_host(
|
||||
hass: HomeAssistant, mock_get_mac_address: Mock
|
||||
) -> None:
|
||||
"""Test getting MAC address from hostname for manual location entry."""
|
||||
# Create device via manual URL entry, so that it must be contacted directly,
|
||||
# not via the ssdp component.
|
||||
DEVICE_HOSTNAME = "local-dmr"
|
||||
DEVICE_LOCATION = f"http://{DEVICE_HOSTNAME}/dmr_description.xml"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_URL: DEVICE_LOCATION}
|
||||
)
|
||||
assert result["data"] == {
|
||||
CONF_URL: DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
}
|
||||
assert result["options"] == {CONF_POLL_AVAILABILITY: True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_get_mac_address.assert_called_once_with(hostname=DEVICE_HOSTNAME)
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry
|
||||
) -> None:
|
||||
|
@ -73,7 +73,7 @@ async def test_event_notifier(
|
||||
|
||||
# Different address should give different notifier
|
||||
listen_addr_3 = EventListenAddr(
|
||||
"192.88.99.4", 9999, "http://192.88.99.4:9999/notify"
|
||||
"198.51.100.4", 9999, "http://198.51.100.4:9999/notify"
|
||||
)
|
||||
event_notifier_3 = await domain_data.async_get_event_notifier(listen_addr_3, hass)
|
||||
assert event_notifier_3 is not None
|
||||
@ -82,8 +82,8 @@ async def test_event_notifier(
|
||||
# Check that the parameters were passed through to the AiohttpNotifyServer
|
||||
aiohttp_notify_servers_mock.assert_called_with(
|
||||
requester=ANY,
|
||||
source=("192.88.99.4", 9999),
|
||||
callback_url="http://192.88.99.4:9999/notify",
|
||||
source=("198.51.100.4", 9999),
|
||||
callback_url="http://198.51.100.4:9999/notify",
|
||||
loop=ANY,
|
||||
)
|
||||
|
||||
|
@ -40,9 +40,18 @@ from homeassistant.components.media_player import (
|
||||
const as mp_const,
|
||||
)
|
||||
from homeassistant.components.media_source import DOMAIN as MS_DOMAIN, PlayMedia
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_MAC,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import async_get as async_get_dr
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
async_get as async_get_dr,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
async_entries_for_config_entry,
|
||||
@ -57,6 +66,7 @@ from .conftest import (
|
||||
MOCK_DEVICE_TYPE,
|
||||
MOCK_DEVICE_UDN,
|
||||
MOCK_DEVICE_USN,
|
||||
MOCK_MAC_ADDRESS,
|
||||
NEW_DEVICE_LOCATION,
|
||||
)
|
||||
|
||||
@ -269,7 +279,7 @@ async def test_setup_entry_with_options(
|
||||
config_entry_mock.options = MappingProxyType(
|
||||
{
|
||||
CONF_LISTEN_PORT: 2222,
|
||||
CONF_CALLBACK_URL_OVERRIDE: "http://192.88.99.10/events",
|
||||
CONF_CALLBACK_URL_OVERRIDE: "http://198.51.100.10/events",
|
||||
CONF_POLL_AVAILABILITY: True,
|
||||
}
|
||||
)
|
||||
@ -283,7 +293,7 @@ async def test_setup_entry_with_options(
|
||||
)
|
||||
# Check event notifiers are acquired with the configured port and callback URL
|
||||
domain_data_mock.async_get_event_notifier.assert_awaited_once_with(
|
||||
EventListenAddr(LOCAL_IP, 2222, "http://192.88.99.10/events"), hass
|
||||
EventListenAddr(LOCAL_IP, 2222, "http://198.51.100.10/events"), hass
|
||||
)
|
||||
# Check UPnP services are subscribed
|
||||
dmr_device_mock.async_subscribe_services.assert_awaited_once_with(
|
||||
@ -324,6 +334,40 @@ async def test_setup_entry_with_options(
|
||||
assert mock_state.state == ha_const.STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_setup_entry_mac_address(
|
||||
hass: HomeAssistant,
|
||||
domain_data_mock: Mock,
|
||||
config_entry_mock: MockConfigEntry,
|
||||
ssdp_scanner_mock: Mock,
|
||||
dmr_device_mock: Mock,
|
||||
) -> None:
|
||||
"""Entry with a MAC address will set up and set the device registry connection."""
|
||||
await setup_mock_component(hass, config_entry_mock)
|
||||
|
||||
# Check the device registry connections for MAC address
|
||||
dev_reg = async_get_dr(hass)
|
||||
device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)})
|
||||
assert device is not None
|
||||
assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections
|
||||
|
||||
|
||||
async def test_setup_entry_no_mac_address(
|
||||
hass: HomeAssistant,
|
||||
domain_data_mock: Mock,
|
||||
config_entry_mock_no_mac: MockConfigEntry,
|
||||
ssdp_scanner_mock: Mock,
|
||||
dmr_device_mock: Mock,
|
||||
) -> None:
|
||||
"""Test setting up an entry without a MAC address will succeed."""
|
||||
await setup_mock_component(hass, config_entry_mock_no_mac)
|
||||
|
||||
# Check the device registry connections does not include the MAC address
|
||||
dev_reg = async_get_dr(hass)
|
||||
device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)})
|
||||
assert device is not None
|
||||
assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections
|
||||
|
||||
|
||||
async def test_event_subscribe_failure(
|
||||
hass: HomeAssistant, config_entry_mock: MockConfigEntry, dmr_device_mock: Mock
|
||||
) -> None:
|
||||
@ -630,21 +674,21 @@ async def test_play_media_stopped(
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_entity_id,
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with(
|
||||
media_url="http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
media_url="http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
media_title="Home Assistant",
|
||||
override_upnp_class="object.item.audioItem.musicTrack",
|
||||
meta_data={},
|
||||
)
|
||||
dmr_device_mock.async_stop.assert_awaited_once_with()
|
||||
dmr_device_mock.async_set_transport_uri.assert_awaited_once_with(
|
||||
"http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
"http://198.51.100.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
)
|
||||
dmr_device_mock.async_wait_for_can_play.assert_awaited_once_with()
|
||||
dmr_device_mock.async_play.assert_awaited_once_with()
|
||||
@ -662,21 +706,21 @@ async def test_play_media_playing(
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_entity_id,
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with(
|
||||
media_url="http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
media_url="http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
media_title="Home Assistant",
|
||||
override_upnp_class="object.item.audioItem.musicTrack",
|
||||
meta_data={},
|
||||
)
|
||||
dmr_device_mock.async_stop.assert_not_awaited()
|
||||
dmr_device_mock.async_set_transport_uri.assert_awaited_once_with(
|
||||
"http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
"http://198.51.100.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
)
|
||||
dmr_device_mock.async_wait_for_can_play.assert_not_awaited()
|
||||
dmr_device_mock.async_play.assert_not_awaited()
|
||||
@ -695,7 +739,7 @@ async def test_play_media_no_autoplay(
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_entity_id,
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False},
|
||||
},
|
||||
@ -703,14 +747,14 @@ async def test_play_media_no_autoplay(
|
||||
)
|
||||
|
||||
dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with(
|
||||
media_url="http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
media_url="http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
media_title="Home Assistant",
|
||||
override_upnp_class="object.item.audioItem.musicTrack",
|
||||
meta_data={},
|
||||
)
|
||||
dmr_device_mock.async_stop.assert_awaited_once_with()
|
||||
dmr_device_mock.async_set_transport_uri.assert_awaited_once_with(
|
||||
"http://192.88.99.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
"http://198.51.100.20:8200/MediaItems/17621.mp3", "Home Assistant", ANY
|
||||
)
|
||||
dmr_device_mock.async_wait_for_can_play.assert_not_awaited()
|
||||
dmr_device_mock.async_play.assert_not_awaited()
|
||||
@ -726,11 +770,11 @@ async def test_play_media_metadata(
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_entity_id,
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
mp_const.ATTR_MEDIA_EXTRA: {
|
||||
"title": "Mock song",
|
||||
"thumb": "http://192.88.99.20:8200/MediaItems/17621.jpg",
|
||||
"thumb": "http://198.51.100.20:8200/MediaItems/17621.jpg",
|
||||
"metadata": {"artist": "Mock artist", "album": "Mock album"},
|
||||
},
|
||||
},
|
||||
@ -738,13 +782,13 @@ async def test_play_media_metadata(
|
||||
)
|
||||
|
||||
dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with(
|
||||
media_url="http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
media_url="http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
media_title="Mock song",
|
||||
override_upnp_class="object.item.audioItem.musicTrack",
|
||||
meta_data={
|
||||
"artist": "Mock artist",
|
||||
"album": "Mock album",
|
||||
"album_art_uri": "http://192.88.99.20:8200/MediaItems/17621.jpg",
|
||||
"album_art_uri": "http://198.51.100.20:8200/MediaItems/17621.jpg",
|
||||
},
|
||||
)
|
||||
|
||||
@ -756,7 +800,7 @@ async def test_play_media_metadata(
|
||||
{
|
||||
ATTR_ENTITY_ID: mock_entity_id,
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/123.mkv",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/123.mkv",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
mp_const.ATTR_MEDIA_EXTRA: {
|
||||
"title": "Mock show",
|
||||
@ -767,7 +811,7 @@ async def test_play_media_metadata(
|
||||
)
|
||||
|
||||
dmr_device_mock.construct_play_media_metadata.assert_awaited_once_with(
|
||||
media_url="http://192.88.99.20:8200/MediaItems/123.mkv",
|
||||
media_url="http://198.51.100.20:8200/MediaItems/123.mkv",
|
||||
media_title="Mock show",
|
||||
override_upnp_class="object.item.videoItem.videoBroadcast",
|
||||
meta_data={"episodeSeason": 1, "episodeNumber": 12},
|
||||
@ -1236,7 +1280,7 @@ async def test_unavailable_device(
|
||||
mp_const.SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://192.88.99.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3",
|
||||
mp_const.ATTR_MEDIA_ENQUEUE: False,
|
||||
},
|
||||
),
|
||||
@ -2146,3 +2190,39 @@ async def test_config_update_poll_availability(
|
||||
mock_state = hass.states.get(mock_entity_id)
|
||||
assert mock_state is not None
|
||||
assert mock_state.state == MediaPlayerState.IDLE
|
||||
|
||||
|
||||
async def test_config_update_mac_address(
|
||||
hass: HomeAssistant,
|
||||
domain_data_mock: Mock,
|
||||
config_entry_mock_no_mac: MockConfigEntry,
|
||||
ssdp_scanner_mock: Mock,
|
||||
dmr_device_mock: Mock,
|
||||
) -> None:
|
||||
"""Test discovering the MAC address post-setup will update the device registry."""
|
||||
await setup_mock_component(hass, config_entry_mock_no_mac)
|
||||
|
||||
domain_data_mock.upnp_factory.async_create_device.reset_mock()
|
||||
|
||||
# Check the device registry connections does not include the MAC address
|
||||
dev_reg = async_get_dr(hass)
|
||||
device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)})
|
||||
assert device is not None
|
||||
assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) not in device.connections
|
||||
|
||||
# MAC address discovered and set by config flow
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry_mock_no_mac,
|
||||
data={
|
||||
CONF_URL: MOCK_DEVICE_LOCATION,
|
||||
CONF_DEVICE_ID: MOCK_DEVICE_UDN,
|
||||
CONF_TYPE: MOCK_DEVICE_TYPE,
|
||||
CONF_MAC: MOCK_MAC_ADDRESS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Device registry connections should now include the MAC address
|
||||
device = dev_reg.async_get_device(identifiers={(DLNA_DOMAIN, MOCK_DEVICE_UDN)})
|
||||
assert device is not None
|
||||
assert (CONNECTION_NETWORK_MAC, MOCK_MAC_ADDRESS) in device.connections
|
||||
|
Reference in New Issue
Block a user