From 2c6e6c2a6fb89d0c602349ad474cb9a861afa42f Mon Sep 17 00:00:00 2001
From: Paulus Schoutsen <paulus@paulusschoutsen.nl>
Date: Thu, 14 Jun 2018 15:17:54 -0400
Subject: [PATCH] Add config entry for Sonos + Cast (#14955)

* Add config entry for Sonos

* Lint

* Use add_job

* Add Cast config entry

* Lint

* Rename DOMAIN import

* Mock pychromecast in test
---
 .coveragerc                                   |   8 +-
 .../components/cast/.translations/en.json     |  15 +++
 homeassistant/components/cast/__init__.py     |  30 +++++
 homeassistant/components/cast/strings.json    |  15 +++
 homeassistant/components/discovery.py         |   4 +-
 .../components/media_player/__init__.py       |  10 ++
 homeassistant/components/media_player/cast.py |  23 +++-
 .../components/media_player/sonos.py          |  25 +++-
 .../components/sonos/.translations/en.json    |  15 +++
 homeassistant/components/sonos/__init__.py    |  29 +++++
 homeassistant/components/sonos/strings.json   |  15 +++
 homeassistant/config_entries.py               |   2 +
 homeassistant/helpers/config_entry_flow.py    |  85 +++++++++++++
 requirements_all.txt                          |   4 +-
 requirements_test_all.txt                     |   2 +-
 tests/components/cast/__init__.py             |   1 +
 tests/components/cast/test_init.py            |  22 ++++
 tests/components/sonos/__init__.py            |   1 +
 tests/components/sonos/test_init.py           |  20 +++
 tests/helpers/test_config_entry_flow.py       | 116 ++++++++++++++++++
 20 files changed, 432 insertions(+), 10 deletions(-)
 create mode 100644 homeassistant/components/cast/.translations/en.json
 create mode 100644 homeassistant/components/cast/__init__.py
 create mode 100644 homeassistant/components/cast/strings.json
 create mode 100644 homeassistant/components/sonos/.translations/en.json
 create mode 100644 homeassistant/components/sonos/__init__.py
 create mode 100644 homeassistant/components/sonos/strings.json
 create mode 100644 homeassistant/helpers/config_entry_flow.py
 create mode 100644 tests/components/cast/__init__.py
 create mode 100644 tests/components/cast/test_init.py
 create mode 100644 tests/components/sonos/__init__.py
 create mode 100644 tests/components/sonos/test_init.py
 create mode 100644 tests/helpers/test_config_entry_flow.py

diff --git a/.coveragerc b/.coveragerc
index 5a8f26e34da..e7d6d2a404a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -61,6 +61,9 @@ omit =
     homeassistant/components/coinbase.py
     homeassistant/components/sensor/coinbase.py
 
+    homeassistant/components/cast/*
+    homeassistant/components/*/cast.py
+
     homeassistant/components/comfoconnect.py
     homeassistant/components/*/comfoconnect.py
 
@@ -252,6 +255,9 @@ omit =
     homeassistant/components/smappee.py
     homeassistant/components/*/smappee.py
 
+    homeassistant/components/sonos/__init__.py
+    homeassistant/components/*/sonos.py
+
     homeassistant/components/tado.py
     homeassistant/components/*/tado.py
 
@@ -482,7 +488,6 @@ omit =
     homeassistant/components/media_player/aquostv.py
     homeassistant/components/media_player/bluesound.py
     homeassistant/components/media_player/braviatv.py
-    homeassistant/components/media_player/cast.py
     homeassistant/components/media_player/channels.py
     homeassistant/components/media_player/clementine.py
     homeassistant/components/media_player/cmus.py
@@ -518,7 +523,6 @@ omit =
     homeassistant/components/media_player/russound_rnet.py
     homeassistant/components/media_player/snapcast.py
     homeassistant/components/media_player/songpal.py
-    homeassistant/components/media_player/sonos.py
     homeassistant/components/media_player/spotify.py
     homeassistant/components/media_player/squeezebox.py
     homeassistant/components/media_player/ue_smart_radio.py
diff --git a/homeassistant/components/cast/.translations/en.json b/homeassistant/components/cast/.translations/en.json
new file mode 100644
index 00000000000..55d79a7d560
--- /dev/null
+++ b/homeassistant/components/cast/.translations/en.json
@@ -0,0 +1,15 @@
+{
+    "config": {
+        "abort": {
+            "no_devices_found": "No Google Cast devices found on the network.",
+            "single_instance_allowed": "Only a single configuration of Google Cast is necessary."
+        },
+        "step": {
+            "confirm": {
+                "description": "Do you want to setup Google Cast?",
+                "title": "Google Cast"
+            }
+        },
+        "title": "Google Cast"
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py
new file mode 100644
index 00000000000..a4ee25f0915
--- /dev/null
+++ b/homeassistant/components/cast/__init__.py
@@ -0,0 +1,30 @@
+"""Component to embed Google Cast."""
+from homeassistant.helpers import config_entry_flow
+
+
+DOMAIN = 'cast'
+REQUIREMENTS = ['pychromecast==2.1.0']
+
+
+async def async_setup(hass, config):
+    """Set up the Cast component."""
+    hass.data[DOMAIN] = config.get(DOMAIN, {})
+    return True
+
+
+async def async_setup_entry(hass, entry):
+    """Set up Cast from a config entry."""
+    hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+        entry, 'media_player'))
+    return True
+
+
+async def _async_has_devices(hass):
+    """Return if there are devices that can be discovered."""
+    from pychromecast.discovery import discover_chromecasts
+
+    return await hass.async_add_job(discover_chromecasts)
+
+
+config_entry_flow.register_discovery_flow(
+    DOMAIN, 'Google Cast', _async_has_devices)
diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json
new file mode 100644
index 00000000000..7f480de0e8b
--- /dev/null
+++ b/homeassistant/components/cast/strings.json
@@ -0,0 +1,15 @@
+{
+  "config": {
+    "title": "Google Cast",
+    "step": {
+      "confirm": {
+        "title": "Google Cast",
+        "description": "Do you want to setup Google Cast?"
+      }
+    },
+    "abort": {
+      "single_instance_allowed": "Only a single configuration of Google Cast is necessary.",
+      "no_devices_found": "No Google Cast devices found on the network."
+    }
+  }
+}
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 00d4291539b..d7041865892 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -46,7 +46,9 @@ SERVICE_HOMEKIT = 'homekit'
 
 CONFIG_ENTRY_HANDLERS = {
     SERVICE_DECONZ: 'deconz',
+    'google_cast': 'cast',
     SERVICE_HUE: 'hue',
+    'sonos': 'sonos',
 }
 
 SERVICE_HANDLERS = {
@@ -64,11 +66,9 @@ SERVICE_HANDLERS = {
     SERVICE_SABNZBD: ('sabnzbd', None),
     SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
     SERVICE_KONNECTED: ('konnected', None),
-    'google_cast': ('media_player', 'cast'),
     'panasonic_viera': ('media_player', 'panasonic_viera'),
     'plex_mediaserver': ('media_player', 'plex'),
     'roku': ('media_player', 'roku'),
-    'sonos': ('media_player', 'sonos'),
     'yamaha': ('media_player', 'yamaha'),
     'logitech_mediaserver': ('media_player', 'squeezebox'),
     'directv': ('media_player', 'directv'),
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 7452b7dd186..d963deba7b5 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -456,6 +456,16 @@ async def async_setup(hass, config):
     return True
 
 
+async def async_setup_entry(hass, entry):
+    """Setup a config entry."""
+    return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+    """Unload a config entry."""
+    return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
 class MediaPlayerDevice(Entity):
     """ABC for media player devices."""
 
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index a9bea9e4c1d..eced0dbbe25 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType, ConfigType
 from homeassistant.core import callback
 from homeassistant.helpers.dispatcher import (dispatcher_send,
                                               async_dispatcher_connect)
+from homeassistant.components.cast import DOMAIN as CAST_DOMAIN
 from homeassistant.components.media_player import (
     MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
     SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
@@ -28,7 +29,7 @@ from homeassistant.const import (
 import homeassistant.helpers.config_validation as cv
 import homeassistant.util.dt as dt_util
 
-REQUIREMENTS = ['pychromecast==2.1.0']
+DEPENDENCIES = ('cast',)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -186,6 +187,26 @@ def _async_create_cast_device(hass: HomeAssistantType,
 
 async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
                                async_add_devices, discovery_info=None):
+    """Set up thet Cast platform.
+
+    Deprecated.
+    """
+    _LOGGER.warning(
+        'Setting configuration for Cast via platform is deprecated. '
+        'Configure via Cast component instead.')
+    await _async_setup_platform(
+        hass, config, async_add_devices, discovery_info)
+
+
+async def async_setup_entry(hass, config_entry, async_add_devices):
+    """Set up Cast from a config entry."""
+    await _async_setup_platform(
+        hass, hass.data[CAST_DOMAIN].get('media_player', {}),
+        async_add_devices, None)
+
+
+async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
+                                async_add_devices, discovery_info):
     """Set up the cast platform."""
     import pychromecast
 
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 0f536e1edfb..da0ad24b135 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -20,13 +20,14 @@ from homeassistant.components.media_player import (
     SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
     SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP,
     SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
+from homeassistant.components.sonos import DOMAIN as SONOS_DOMAIN
 from homeassistant.const import (
     ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED,
     STATE_PLAYING)
 import homeassistant.helpers.config_validation as cv
 from homeassistant.util.dt import utcnow
 
-REQUIREMENTS = ['SoCo==0.14']
+DEPENDENCIES = ('sonos',)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -49,7 +50,7 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
 SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
 SERVICE_SET_OPTION = 'sonos_set_option'
 
-DATA_SONOS = 'sonos'
+DATA_SONOS = 'sonos_devices'
 
 SOURCE_LINEIN = 'Line-in'
 SOURCE_TV = 'TV'
@@ -118,6 +119,26 @@ class SonosData:
 
 
 def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the Sonos platform.
+
+    Deprecated.
+    """
+    _LOGGER.warning('Loading Sonos via platform config is deprecated.')
+    _setup_platform(hass, config, add_devices, discovery_info)
+
+
+async def async_setup_entry(hass, config_entry, async_add_devices):
+    """Set up Sonos from a config entry."""
+    def add_devices(devices, update_before_add=False):
+        """Sync version of async add devices."""
+        hass.add_job(async_add_devices, devices, update_before_add)
+
+    hass.add_job(_setup_platform, hass,
+                 hass.data[SONOS_DOMAIN].get('media_player', {}),
+                 add_devices, None)
+
+
+def _setup_platform(hass, config, add_devices, discovery_info):
     """Set up the Sonos platform."""
     import soco
     import soco.events
diff --git a/homeassistant/components/sonos/.translations/en.json b/homeassistant/components/sonos/.translations/en.json
new file mode 100644
index 00000000000..c7aae4302f6
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/en.json
@@ -0,0 +1,15 @@
+{
+    "config": {
+        "abort": {
+            "no_devices_found": "No Sonos devices found on the network.",
+            "single_instance_allowed": "Only a single configuration of Sonos is necessary."
+        },
+        "step": {
+            "confirm": {
+                "description": "Do you want to setup Sonos?",
+                "title": "Sonos"
+            }
+        },
+        "title": "Sonos"
+    }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
new file mode 100644
index 00000000000..7c3de210768
--- /dev/null
+++ b/homeassistant/components/sonos/__init__.py
@@ -0,0 +1,29 @@
+"""Component to embed Sonos."""
+from homeassistant.helpers import config_entry_flow
+
+
+DOMAIN = 'sonos'
+REQUIREMENTS = ['SoCo==0.14']
+
+
+async def async_setup(hass, config):
+    """Set up the Sonos component."""
+    hass.data[DOMAIN] = config.get(DOMAIN, {})
+    return True
+
+
+async def async_setup_entry(hass, entry):
+    """Set up Sonos from a config entry."""
+    hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+        entry, 'media_player'))
+    return True
+
+
+async def _async_has_devices(hass):
+    """Return if there are devices that can be discovered."""
+    import soco
+
+    return await hass.async_add_job(soco.discover)
+
+
+config_entry_flow.register_discovery_flow(DOMAIN, 'Sonos', _async_has_devices)
diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json
new file mode 100644
index 00000000000..4aa68712d59
--- /dev/null
+++ b/homeassistant/components/sonos/strings.json
@@ -0,0 +1,15 @@
+{
+  "config": {
+    "title": "Sonos",
+    "step": {
+      "confirm": {
+        "title": "Sonos",
+        "description": "Do you want to setup Sonos?"
+      }
+    },
+    "abort": {
+      "single_instance_allowed": "Only a single configuration of Sonos is necessary.",
+      "no_devices_found": "No Sonos devices found on the network."
+    }
+  }
+}
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 7826e26b960..504c0850a93 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -127,9 +127,11 @@ _LOGGER = logging.getLogger(__name__)
 HANDLERS = Registry()
 # Components that have config flows. In future we will auto-generate this list.
 FLOWS = [
+    'cast',
     'deconz',
     'hue',
     'nest',
+    'sonos',
     'zone',
 ]
 
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
new file mode 100644
index 00000000000..2a4ec2966df
--- /dev/null
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -0,0 +1,85 @@
+"""Helpers for data entry flows for config entries."""
+from functools import partial
+
+from homeassistant.core import callback
+from homeassistant import config_entries, data_entry_flow
+
+
+def register_discovery_flow(domain, title, discovery_function):
+    """Register flow for discovered integrations that not require auth."""
+    config_entries.HANDLERS.register(domain)(
+        partial(DiscoveryFlowHandler, domain, title, discovery_function))
+
+
+class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
+    """Handle a discovery config flow."""
+
+    VERSION = 1
+
+    def __init__(self, domain, title, discovery_function):
+        """Initialize the discovery config flow."""
+        self._domain = domain
+        self._title = title
+        self._discovery_function = discovery_function
+
+    async def async_step_init(self, user_input=None):
+        """Handle a flow initialized by the user."""
+        if self._async_current_entries():
+            return self.async_abort(
+                reason='single_instance_allowed'
+            )
+
+        # Get current discovered entries.
+        in_progress = self._async_in_progress()
+
+        has_devices = in_progress
+        if not has_devices:
+            has_devices = await self.hass.async_add_job(
+                self._discovery_function, self.hass)
+
+        if not has_devices:
+            return self.async_abort(
+                reason='no_devices_found'
+            )
+
+        # Cancel the discovered one.
+        for flow in in_progress:
+            self.hass.config_entries.flow.async_abort(flow['flow_id'])
+
+        return self.async_create_entry(
+            title=self._title,
+            data={},
+        )
+
+    async def async_step_confirm(self, user_input=None):
+        """Confirm setup."""
+        if user_input is not None:
+            return self.async_create_entry(
+                title=self._title,
+                data={},
+            )
+
+        return self.async_show_form(
+            step_id='confirm',
+        )
+
+    async def async_step_discovery(self, discovery_info):
+        """Handle a flow initialized by discovery."""
+        if self._async_in_progress() or self._async_current_entries():
+            return self.async_abort(
+                reason='single_instance_allowed'
+            )
+
+        return await self.async_step_confirm()
+
+    @callback
+    def _async_current_entries(self):
+        """Return current entries."""
+        return self.hass.config_entries.async_entries(self._domain)
+
+    @callback
+    def _async_in_progress(self):
+        """Return other in progress flows for current domain."""
+        return [flw for flw in self.hass.config_entries.flow.async_progress()
+                if flw['handler'] == self._domain and
+                flw['flow_id'] != self.flow_id]
diff --git a/requirements_all.txt b/requirements_all.txt
index f144f3e2a2b..79db358942d 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -54,7 +54,7 @@ PyXiaomiGateway==0.9.5
 # homeassistant.components.remember_the_milk
 RtmAPI==0.7.0
 
-# homeassistant.components.media_player.sonos
+# homeassistant.components.sonos
 SoCo==0.14
 
 # homeassistant.components.sensor.travisci
@@ -773,7 +773,7 @@ pyblackbird==0.5
 # homeassistant.components.media_player.channels
 pychannels==1.0.0
 
-# homeassistant.components.media_player.cast
+# homeassistant.components.cast
 pychromecast==2.1.0
 
 # homeassistant.components.media_player.cmus
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 82adbcc0733..34928b4f111 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -24,7 +24,7 @@ HAP-python==2.2.2
 # homeassistant.components.notify.html5
 PyJWT==1.6.0
 
-# homeassistant.components.media_player.sonos
+# homeassistant.components.sonos
 SoCo==0.14
 
 # homeassistant.components.device_tracker.automatic
diff --git a/tests/components/cast/__init__.py b/tests/components/cast/__init__.py
new file mode 100644
index 00000000000..7e904dce00a
--- /dev/null
+++ b/tests/components/cast/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Cast component."""
diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py
new file mode 100644
index 00000000000..260856c6742
--- /dev/null
+++ b/tests/components/cast/test_init.py
@@ -0,0 +1,22 @@
+"""Tests for the Cast config flow."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.components import cast
+
+from tests.common import MockDependency, mock_coro
+
+
+async def test_creating_entry_sets_up_media_player(hass):
+    """Test setting up Cast loads the media player."""
+    with patch('homeassistant.components.media_player.cast.async_setup_entry',
+               return_value=mock_coro(True)) as mock_setup, \
+            MockDependency('pychromecast', 'discovery'), \
+            patch('pychromecast.discovery.discover_chromecasts',
+                  return_value=True):
+        result = await hass.config_entries.flow.async_init(cast.DOMAIN)
+        assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+        await hass.async_block_till_done()
+
+    assert len(mock_setup.mock_calls) == 1
diff --git a/tests/components/sonos/__init__.py b/tests/components/sonos/__init__.py
new file mode 100644
index 00000000000..878e0c17318
--- /dev/null
+++ b/tests/components/sonos/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Sonos component."""
diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py
new file mode 100644
index 00000000000..2cbc2360fd4
--- /dev/null
+++ b/tests/components/sonos/test_init.py
@@ -0,0 +1,20 @@
+"""Tests for the Sonos config flow."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.components import sonos
+
+from tests.common import mock_coro
+
+
+async def test_creating_entry_sets_up_media_player(hass):
+    """Test setting up Sonos loads the media player."""
+    with patch('homeassistant.components.media_player.sonos.async_setup_entry',
+               return_value=mock_coro(True)) as mock_setup, \
+            patch('soco.discover', return_value=True):
+        result = await hass.config_entries.flow.async_init(sonos.DOMAIN)
+        assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+        await hass.async_block_till_done()
+
+    assert len(mock_setup.mock_calls) == 1
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
new file mode 100644
index 00000000000..d3f13ac4302
--- /dev/null
+++ b/tests/helpers/test_config_entry_flow.py
@@ -0,0 +1,116 @@
+"""Tests for the Config Entry Flow helper."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow, loader
+from homeassistant.helpers import config_entry_flow
+from tests.common import MockConfigEntry, MockModule
+
+
+@pytest.fixture
+def flow_conf(hass):
+    """Register a handler."""
+    handler_conf = {
+        'discovered': False,
+    }
+
+    async def has_discovered_devices(hass):
+        """Mock if we have discovered devices."""
+        return handler_conf['discovered']
+
+    with patch.dict(config_entries.HANDLERS):
+        config_entry_flow.register_discovery_flow(
+            'test', 'Test', has_discovered_devices)
+        yield handler_conf
+
+
+async def test_single_entry_allowed(hass, flow_conf):
+    """Test only a single entry is allowed."""
+    flow = config_entries.HANDLERS['test']()
+    flow.hass = hass
+
+    MockConfigEntry(domain='test').add_to_hass(hass)
+    result = await flow.async_step_init()
+
+    assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_user_no_devices_found(hass, flow_conf):
+    """Test if no devices found."""
+    flow = config_entries.HANDLERS['test']()
+    flow.hass = hass
+
+    result = await flow.async_step_init()
+
+    assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result['reason'] == 'no_devices_found'
+
+
+async def test_user_no_confirmation(hass, flow_conf):
+    """Test user requires no confirmation to setup."""
+    flow = config_entries.HANDLERS['test']()
+    flow.hass = hass
+    flow_conf['discovered'] = True
+
+    result = await flow.async_step_init()
+
+    assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_discovery_single_instance(hass, flow_conf):
+    """Test we ask for confirmation via discovery."""
+    flow = config_entries.HANDLERS['test']()
+    flow.hass = hass
+
+    MockConfigEntry(domain='test').add_to_hass(hass)
+    result = await flow.async_step_discovery({})
+
+    assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+    assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_discovery_confirmation(hass, flow_conf):
+    """Test we ask for confirmation via discovery."""
+    flow = config_entries.HANDLERS['test']()
+    flow.hass = hass
+
+    result = await flow.async_step_discovery({})
+
+    assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+    assert result['step_id'] == 'confirm'
+
+    result = await flow.async_step_confirm({})
+    assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_multiple_discoveries(hass, flow_conf):
+    """Test we only create one instance for multiple discoveries."""
+    loader.set_component(hass, 'test', MockModule('test'))
+
+    result = await hass.config_entries.flow.async_init(
+        'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+    assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+    # Second discovery
+    result = await hass.config_entries.flow.async_init(
+        'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+    assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+
+
+async def test_user_init_trumps_discovery(hass, flow_conf):
+    """Test a user initialized one will finish and cancel discovered one."""
+    loader.set_component(hass, 'test', MockModule('test'))
+
+    # Discovery starts flow
+    result = await hass.config_entries.flow.async_init(
+        'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+    assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+    # User starts flow
+    result = await hass.config_entries.flow.async_init('test', data={})
+    assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+    # Discovery flow has been aborted
+    assert len(hass.config_entries.flow.async_progress()) == 0
-- 
GitLab