dlna_dmr won't support devices that don't provide all DMR services (#58374)

pull/58450/head
Michael Chisholm 2021-10-26 11:54:58 +11:00 committed by GitHub
parent a9a74e0415
commit 44aa1fdc66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 12 deletions

View File

@ -216,6 +216,19 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if _is_ignored_device(discovery_info):
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.get(ssdp.ATTR_UPNP_SERVICE_LIST)
if not discovery_service_list:
return self.async_abort(reason="not_dmr")
discovery_service_ids = {
service.get("serviceId")
for service in discovery_service_list.get("service") or []
}
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
return self.async_abort(reason="not_dmr")
# 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:
@ -277,10 +290,10 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except UpnpError as err:
raise ConnectError("cannot_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 DmrDevice.is_profile_device(device):
raise ConnectError("not_dmr")
device = find_device_of_type(device, DmrDevice.DEVICE_TYPES)
if not self._udn:
self._udn = device.udn

View File

@ -30,11 +30,11 @@
"discovery_error": "Failed to discover a matching DLNA device",
"incomplete_config": "Configuration is missing a required variable",
"non_unique_id": "Multiple devices found with the same unique ID",
"not_dmr": "Device is not a Digital Media Renderer"
"not_dmr": "Device is not a supported Digital Media Renderer"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_dmr": "Device is not a Digital Media Renderer"
"not_dmr": "Device is not a supported Digital Media Renderer"
}
},
"options": {

View File

@ -8,12 +8,12 @@
"discovery_error": "Failed to discover a matching DLNA device",
"incomplete_config": "Configuration is missing a required variable",
"non_unique_id": "Multiple devices found with the same unique ID",
"not_dmr": "Device is not a Digital Media Renderer"
"not_dmr": "Device is not a supported Digital Media Renderer"
},
"error": {
"cannot_connect": "Failed to connect",
"could_not_connect": "Failed to connect to DLNA device",
"not_dmr": "Device is not a Digital Media Renderer"
"not_dmr": "Device is not a supported Digital Media Renderer"
},
"flow_title": "{name}",
"step": {

View File

@ -51,6 +51,7 @@ ATTR_UPNP_MODEL_NAME = "modelName"
ATTR_UPNP_MODEL_NUMBER = "modelNumber"
ATTR_UPNP_MODEL_URL = "modelURL"
ATTR_UPNP_SERIAL = "serialNumber"
ATTR_UPNP_SERVICE_LIST = "serviceList"
ATTR_UPNP_UDN = "UDN"
ATTR_UPNP_UPC = "UPC"
ATTR_UPNP_PRESENTATION_URL = "presentationURL"

View File

@ -5,7 +5,7 @@ from collections.abc import Iterable
from socket import AddressFamily # pylint: disable=no-name-in-module
from unittest.mock import Mock, create_autospec, patch, seal
from async_upnp_client import UpnpDevice, UpnpFactory
from async_upnp_client import UpnpDevice, UpnpFactory, UpnpService
import pytest
from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN
@ -49,6 +49,26 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]:
upnp_device.parent_device = None
upnp_device.root_device = upnp_device
upnp_device.all_devices = [upnp_device]
upnp_device.services = {
"urn:schemas-upnp-org:service:AVTransport:1": create_autospec(
UpnpService,
instance=True,
service_type="urn:schemas-upnp-org:service:AVTransport:1",
service_id="urn:upnp-org:serviceId:AVTransport",
),
"urn:schemas-upnp-org:service:ConnectionManager:1": create_autospec(
UpnpService,
instance=True,
service_type="urn:schemas-upnp-org:service:ConnectionManager:1",
service_id="urn:upnp-org:serviceId:ConnectionManager",
),
"urn:schemas-upnp-org:service:RenderingControl:1": create_autospec(
UpnpService,
instance=True,
service_type="urn:schemas-upnp-org:service:RenderingControl:1",
service_id="urn:upnp-org:serviceId:RenderingControl",
),
}
seal(upnp_device)
domain_data.upnp_factory.async_create_device.return_value = upnp_device

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
CONF_URL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import DiscoveryInfoType
from .conftest import (
MOCK_DEVICE_LOCATION,
@ -51,13 +52,38 @@ MOCK_CONFIG_IMPORT_DATA = {
MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE"
MOCK_DISCOVERY = {
MOCK_DISCOVERY: DiscoveryInfoType = {
ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION,
ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN,
ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE,
ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN,
ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE,
ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME,
ssdp.ATTR_UPNP_SERVICE_LIST: {
"service": [
{
"SCPDURL": "/AVTransport/scpd.xml",
"controlURL": "/AVTransport/control.xml",
"eventSubURL": "/AVTransport/event.xml",
"serviceId": "urn:upnp-org:serviceId:AVTransport",
"serviceType": "urn:schemas-upnp-org:service:AVTransport:1",
},
{
"SCPDURL": "/ConnectionManager/scpd.xml",
"controlURL": "/ConnectionManager/control.xml",
"eventSubURL": "/ConnectionManager/event.xml",
"serviceId": "urn:upnp-org:serviceId:ConnectionManager",
"serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1",
},
{
"SCPDURL": "/RenderingControl/scpd.xml",
"controlURL": "/RenderingControl/control.xml",
"eventSubURL": "/RenderingControl/event.xml",
"serviceId": "urn:upnp-org:serviceId:RenderingControl",
"serviceType": "urn:schemas-upnp-org:service:RenderingControl:1",
},
]
},
ssdp.ATTR_HA_MATCHING_DOMAINS: {DLNA_DOMAIN},
}
@ -197,6 +223,8 @@ async def test_user_flow_embedded_st(
embedded_device.udn = MOCK_DEVICE_UDN
embedded_device.device_type = MOCK_DEVICE_TYPE
embedded_device.name = MOCK_DEVICE_NAME
embedded_device.services = upnp_device.services
upnp_device.services = {}
upnp_device.all_devices.append(embedded_device)
result = await hass.config_entries.flow.async_init(
@ -552,9 +580,38 @@ async def test_ssdp_flow_upnp_udn(
assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION
async def test_ssdp_missing_services(hass: HomeAssistant) -> None:
"""Test SSDP ignores devices that are missing required services."""
# No services defined at all
discovery = dict(MOCK_DISCOVERY)
del discovery[ssdp.ATTR_UPNP_SERVICE_LIST]
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"] == "not_dmr"
# AVTransport service is missing
discovery = dict(MOCK_DISCOVERY)
discovery[ssdp.ATTR_UPNP_SERVICE_LIST] = {
"service": [
service
for service in discovery[ssdp.ATTR_UPNP_SERVICE_LIST]["service"]
if service.get("serviceId") != "urn:upnp-org:serviceId:AVTransport"
]
}
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"] == "not_dmr"
async def test_ssdp_ignore_device(hass: HomeAssistant) -> None:
"""Test SSDP discovery ignores certain devices."""
discovery = MOCK_DISCOVERY.copy()
discovery = dict(MOCK_DISCOVERY)
discovery[ssdp.ATTR_HA_MATCHING_DOMAINS] = {DLNA_DOMAIN, "other_domain"}
result = await hass.config_entries.flow.async_init(
DLNA_DOMAIN,
@ -564,7 +621,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None:
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "alternative_integration"
discovery = MOCK_DISCOVERY.copy()
discovery = dict(MOCK_DISCOVERY)
discovery[ssdp.ATTR_UPNP_DEVICE_TYPE] = "urn:schemas-upnp-org:device:ZonePlayer:1"
result = await hass.config_entries.flow.async_init(
DLNA_DOMAIN,