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