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