diff --git a/.coveragerc b/.coveragerc index f7b252ad6e8..95bd72253c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -679,7 +679,8 @@ omit = homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py - homeassistant/components/songpal/* + homeassistant/components/songpal/__init__.py + homeassistant/components/songpal/media_player.py homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* diff --git a/CODEOWNERS b/CODEOWNERS index 1f53b1292b0..e518f19c9f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -362,7 +362,7 @@ homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/sonarr/* @ctalkington -homeassistant/components/songpal/* @rytilahti +homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 87c0e41533b..afcf8cc341d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -71,7 +71,6 @@ SERVICE_HANDLERS = { "openhome": ("media_player", "openhome"), "bose_soundtouch": ("media_player", "soundtouch"), "bluesound": ("media_player", "bluesound"), - "songpal": ("media_player", "songpal"), "kodi": ("media_player", "kodi"), "volumio": ("media_player", "volumio"), "lg_smart_device": ("media_player", "lg_soundbar"), @@ -91,6 +90,7 @@ MIGRATED_SERVICE_HANDLERS = [ "ikea_tradfri", "philips_hue", "sonos", + "songpal", SERVICE_WEMO, ] diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 7b181d375a5..4a4332cb0a5 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -1 +1,50 @@ """The songpal component.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import CONF_ENDPOINT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SONGPAL_CONFIG_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [SONGPAL_CONFIG_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool: + """Set up songpal environment.""" + conf = config.get(DOMAIN) + if conf is None: + return True + for config_entry in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry, + ), + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up songpal media player.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "media_player") + ) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload songpal media player.""" + return await hass.config_entries.async_forward_entry_unload(entry, "media_player") diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py new file mode 100644 index 00000000000..206cfce575d --- /dev/null +++ b/homeassistant/components/songpal/config_flow.py @@ -0,0 +1,153 @@ +"""Config flow to configure songpal component.""" +import logging +from typing import Optional +from urllib.parse import urlparse + +from songpal import Device, SongpalException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST, CONF_NAME + +from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class SongpalConfig: + """Device Configuration.""" + + def __init__(self, name, host, endpoint): + """Initialize Configuration.""" + self.name = name + self.host = host + self.endpoint = endpoint + + +class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Songpal configuration flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the flow.""" + self.conf: Optional[SongpalConfig] = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_ENDPOINT): str}), + ) + + # Validate input + endpoint = user_input[CONF_ENDPOINT] + parsed_url = urlparse(endpoint) + + # Try to connect and get device name + try: + device = Device(endpoint) + await device.get_supported_methods() + interface_info = await device.get_interface_information() + name = interface_info.modelName + except SongpalException as ex: + _LOGGER.debug("Connection failed: %s", ex) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_ENDPOINT, default=user_input.get(CONF_ENDPOINT, "") + ): str, + } + ), + errors={"base": "connection"}, + ) + + self.conf = SongpalConfig(name, parsed_url.hostname, endpoint) + + return await self.async_step_init(user_input) + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + # Check if already configured + if self._endpoint_already_configured(): + return self.async_abort(reason="already_configured") + + if user_input is None: + return self.async_show_form( + step_id="init", + description_placeholders={ + CONF_NAME: self.conf.name, + CONF_HOST: self.conf.host, + }, + ) + + await self.async_set_unique_id(self.conf.endpoint) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.conf.name, + data={CONF_NAME: self.conf.name, CONF_ENDPOINT: self.conf.endpoint}, + ) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered Songpal device.""" + await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured() + + _LOGGER.debug("Discovered: %s", discovery_info) + + friendly_name = discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME] + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + scalarweb_info = discovery_info["X_ScalarWebAPI_DeviceInfo"] + endpoint = scalarweb_info["X_ScalarWebAPI_BaseURL"] + service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ] + + # Ignore Bravia TVs + if "videoScreen" in service_types: + return self.async_abort(reason="not_songpal_device") + + # pylint: disable=no-member + self.context["title_placeholders"] = { + CONF_NAME: friendly_name, + CONF_HOST: parsed_url.hostname, + } + + self.conf = SongpalConfig(friendly_name, parsed_url.hostname, endpoint) + + return await self.async_step_init() + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + name = user_input.get(CONF_NAME) + endpoint = user_input.get(CONF_ENDPOINT) + parsed_url = urlparse(endpoint) + + # Try to connect to test the endpoint + try: + device = Device(endpoint) + await device.get_supported_methods() + # Get name + if name is None: + interface_info = await device.get_interface_information() + name = interface_info.modelName + except SongpalException as ex: + _LOGGER.error("Import from yaml configuration failed: %s", ex) + return self.async_abort(reason="connection") + + self.conf = SongpalConfig(name, parsed_url.hostname, endpoint) + + return await self.async_step_init(user_input) + + def _endpoint_already_configured(self): + """See if we already have an endpoint matching user input configured.""" + existing_endpoints = [ + entry.data[CONF_ENDPOINT] for entry in self._async_current_entries() + ] + return self.conf.endpoint in existing_endpoints diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py index 6a19e316a9f..f12b77800a9 100644 --- a/homeassistant/components/songpal/const.py +++ b/homeassistant/components/songpal/const.py @@ -1,3 +1,5 @@ """Constants for the Songpal component.""" DOMAIN = "songpal" SET_SOUND_SETTING = "set_sound_setting" + +CONF_ENDPOINT = "endpoint" diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 583f0dff6ef..162ec7c2147 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,7 +1,14 @@ { "domain": "songpal", "name": "Sony Songpal", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.11.2"], - "codeowners": ["@rytilahti"] + "requirements": ["python-songpal==0.12"], + "codeowners": ["@rytilahti", "@shenxn"], + "ssdp": [ + { + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + "manufacturer": "Sony Corporation" + } + ] } diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 55d8f0133a9..5894faa5e2e 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -13,7 +13,7 @@ from songpal import ( ) import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, @@ -30,19 +31,16 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, SET_SOUND_SETTING +from .const import CONF_ENDPOINT, DOMAIN, SET_SOUND_SETTING _LOGGER = logging.getLogger(__name__) -CONF_ENDPOINT = "endpoint" - PARAM_NAME = "name" PARAM_VALUE = "value" -PLATFORM = "songpal" - SUPPORT_SONGPAL = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP @@ -52,10 +50,6 @@ SUPPORT_SONGPAL = ( | SUPPORT_TURN_OFF ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ENDPOINT): cv.string} -) - SET_SOUND_SCHEMA = vol.Schema( { vol.Optional(ATTR_ENTITY_ID): cv.entity_id, @@ -65,33 +59,37 @@ SET_SOUND_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Songpal platform.""" - if PLATFORM not in hass.data: - hass.data[PLATFORM] = {} +async def async_setup_platform( + hass: HomeAssistantType, config: dict, async_add_entities, discovery_info=None +) -> None: + """Set up from legacy configuration file. Obsolete.""" + _LOGGER.error( + "Configuring Songpal through media_player platform is no longer supported. Convert to songpal platform or UI configuration." + ) - if discovery_info is not None: - name = discovery_info["name"] - endpoint = discovery_info["properties"]["endpoint"] - _LOGGER.debug("Got autodiscovered %s - endpoint: %s", name, endpoint) - device = SongpalDevice(name, endpoint) - else: - name = config.get(CONF_NAME) - endpoint = config.get(CONF_ENDPOINT) - device = SongpalDevice(name, endpoint, poll=False) +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up songpal media player.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} - if endpoint in hass.data[PLATFORM]: + name = config_entry.data[CONF_NAME] + endpoint = config_entry.data[CONF_ENDPOINT] + + if endpoint in hass.data[DOMAIN]: _LOGGER.debug("The endpoint exists already, skipping setup.") return + device = SongpalDevice(name, endpoint) try: await device.initialize() except SongpalException as ex: _LOGGER.error("Unable to get methods from songpal: %s", ex) raise PlatformNotReady - hass.data[PLATFORM][endpoint] = device + hass.data[DOMAIN][endpoint] = device async_add_entities([device], True) @@ -102,7 +100,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID } - for device in hass.data[PLATFORM].values(): + for device in hass.data[DOMAIN].values(): if device.entity_id == entity_id or entity_id is None: _LOGGER.debug( "Calling %s (entity: %s) with params %s", service, entity_id, params @@ -127,6 +125,7 @@ class SongpalDevice(MediaPlayerEntity): self._poll = poll self.dev = Device(self._endpoint) self._sysinfo = None + self._model = None self._state = False self._available = False @@ -150,6 +149,13 @@ class SongpalDevice(MediaPlayerEntity): """Initialize the device.""" await self.dev.get_supported_methods() self._sysinfo = await self.dev.get_system_info() + interface_info = await self.dev.get_interface_information() + self._model = interface_info.modelName + + async def async_will_remove_from_hass(self): + """Run when entity will be removed from hass.""" + self.hass.data[DOMAIN].pop(self._endpoint) + await self.dev.stop_listen_notifications() async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" @@ -221,6 +227,18 @@ class SongpalDevice(MediaPlayerEntity): """Return a unique ID.""" return self._sysinfo.macAddr + @property + def device_info(self): + """Return the device info.""" + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self._sysinfo.macAddr)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": "Sony Corporation", + "name": self.name, + "sw_version": self._sysinfo.version, + "model": self._model, + } + @property def available(self): """Return availability of the device.""" diff --git a/homeassistant/components/songpal/strings.json b/homeassistant/components/songpal/strings.json new file mode 100644 index 00000000000..7948e99af29 --- /dev/null +++ b/homeassistant/components/songpal/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "user": { + "data": { + "endpoint": "Endpoint" + }, + "title": "Sony Songpal" + }, + "init": { + "description": "Do you want to set up {name} ({host})?", + "title": "Sony Songpal" + } + }, + "error": { + "connection": "Connection error: please check your endpoint" + }, + "abort": { + "already_configured": "Device already configured", + "not_songpal_device": "Not a Songpal device" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 015e240d766..9189af4cdd3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -120,6 +120,7 @@ FLOWS = [ "solarlog", "soma", "somfy", + "songpal", "sonos", "spotify", "starline", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index f46ba1611a8..52134888c0c 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -70,6 +70,12 @@ SSDP = { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], + "songpal": [ + { + "manufacturer": "Sony Corporation", + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1" + } + ], "sonos": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index 4663707abe7..45a658b72f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11.2 +python-songpal==0.12 # homeassistant.components.synology_dsm python-synology==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76499a31827..b7ac76068ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,6 +677,9 @@ python-nest==4.1.0 # homeassistant.components.zwave_mqtt python-openzwave-mqtt==1.0.1 +# homeassistant.components.songpal +python-songpal==0.12 + # homeassistant.components.synology_dsm python-synology==0.8.0 diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py new file mode 100644 index 00000000000..24729c1e8cc --- /dev/null +++ b/tests/components/songpal/__init__.py @@ -0,0 +1 @@ +"""Test the songpal integration.""" diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py new file mode 100644 index 00000000000..c83faf42699 --- /dev/null +++ b/tests/components/songpal/test_config_flow.py @@ -0,0 +1,249 @@ +"""Test the songpal config flow.""" +import copy + +from asynctest import MagicMock, patch +from songpal import SongpalException +from songpal.containers import InterfaceInfo + +from homeassistant.components import ssdp +from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + +UDN = "uuid:1234" +FRIENDLY_NAME = "friendly name" +HOST = "0.0.0.0" +ENDPOINT = f"http://{HOST}:10000/sony" +MODEL = "model" + +SSDP_DATA = { + ssdp.ATTR_UPNP_UDN: UDN, + ssdp.ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, + ssdp.ATTR_SSDP_LOCATION: f"http://{HOST}:52323/dmr.xml", + "X_ScalarWebAPI_DeviceInfo": { + "X_ScalarWebAPI_BaseURL": ENDPOINT, + "X_ScalarWebAPI_ServiceList": { + "X_ScalarWebAPI_ServiceType": ["guide", "system", "audio", "avContent"], + }, + }, +} + +CONF_DATA = { + CONF_NAME: FRIENDLY_NAME, + CONF_ENDPOINT: ENDPOINT, +} + + +async def _async_return_value(): + pass + + +def _get_supported_methods(throw_exception): + def get_supported_methods(): + if throw_exception: + raise SongpalException("Unable to do POST request: ") + return _async_return_value() + + return get_supported_methods + + +async def _get_interface_information(): + return InterfaceInfo( + productName="product name", + modelName=MODEL, + productCategory="product category", + interfaceVersion="interface version", + serverName="server name", + ) + + +def _create_mocked_device(throw_exception=False): + mocked_device = MagicMock() + type(mocked_device).get_supported_methods = MagicMock( + side_effect=_get_supported_methods(throw_exception) + ) + type(mocked_device).get_interface_information = MagicMock( + side_effect=_get_interface_information + ) + return mocked_device + + +def _patch_config_flow_device(mocked_device): + return patch( + "homeassistant.components.songpal.config_flow.Device", + return_value=mocked_device, + ) + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) + + +async def test_flow_ssdp(hass): + """Test working ssdp flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + ) + assert result["type"] == "form" + assert result["step_id"] == "init" + assert result["description_placeholders"] == { + CONF_NAME: FRIENDLY_NAME, + CONF_HOST: HOST, + } + flow = _flow_next(hass, result["flow_id"]) + assert flow["context"]["unique_id"] == UDN + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FRIENDLY_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user(hass): + """Test working user initialized flow.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] is None + _flow_next(hass, result["flow_id"]) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MODEL + assert result["data"] == { + CONF_NAME: MODEL, + CONF_ENDPOINT: ENDPOINT, + } + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_called_once() + + +async def test_flow_import(hass): + """Test working import flow.""" + mocked_device = _create_mocked_device() + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FRIENDLY_NAME + assert result["data"] == CONF_DATA + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +def _create_mock_config_entry(hass): + MockConfigEntry(domain=DOMAIN, unique_id="uuid:0000", data=CONF_DATA,).add_to_hass( + hass + ) + + +async def test_ssdp_bravia(hass): + """Test discovering a bravia TV.""" + ssdp_data = copy.deepcopy(SSDP_DATA) + ssdp_data["X_ScalarWebAPI_DeviceInfo"]["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ].append("videoScreen") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=ssdp_data, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_songpal_device" + + +async def test_sddp_exist(hass): + """Test discovering existed device.""" + _create_mock_config_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_exist(hass): + """Test user adding existed device.""" + mocked_device = _create_mocked_device() + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_called_once() + + +async def test_import_exist(hass): + """Test importing existed device.""" + mocked_device = _create_mocked_device() + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +async def test_user_invalid(hass): + """Test using adding invalid config.""" + mocked_device = _create_mocked_device(True) + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "connection"} + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called() + + +async def test_import_invalid(hass): + """Test importing invalid config.""" + mocked_device = _create_mocked_device(True) + _create_mock_config_entry(hass) + + with _patch_config_flow_device(mocked_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "connection" + + mocked_device.get_supported_methods.assert_called_once() + mocked_device.get_interface_information.assert_not_called()