From 078ce6766e17d197efcf03fe0182fb46836ddc3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2020 12:22:35 -0500 Subject: [PATCH] Add discovery support to synology_dsm (#33729) * Add discovery support to synology_dsm * Remove debug * black * Fix mocks * Merge strings * Move placeholders * add missing placeholders * reorder * use constants in test * Remove CONF_NAME (only displayed in discovery now) * test reduction * Shorten long name * Use name for name but NOT for unique_id, remove CONF_NAME from yaml config * Use Synology for the name for now, hopefully we can use the hostname later * lint * Remove name from strings * remove =None * show login errors at username field * Update tests/components/synology_dsm/test_config_flow.py Co-Authored-By: Quentame * Update tests/components/synology_dsm/test_config_flow.py Co-Authored-By: Quentame * Update homeassistant/components/synology_dsm/const.py Co-authored-by: Quentame --- .../synology_dsm/.translations/en.json | 59 +++--- .../components/synology_dsm/__init__.py | 4 +- .../components/synology_dsm/config_flow.py | 182 ++++++++++++------ .../components/synology_dsm/const.py | 2 +- .../components/synology_dsm/manifest.json | 8 +- .../components/synology_dsm/sensor.py | 18 +- .../components/synology_dsm/strings.json | 15 +- homeassistant/generated/ssdp.py | 6 + .../synology_dsm/test_config_flow.py | 69 +++++-- 9 files changed, 243 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/synology_dsm/.translations/en.json b/homeassistant/components/synology_dsm/.translations/en.json index 327745343ba..77bd1250033 100644 --- a/homeassistant/components/synology_dsm/.translations/en.json +++ b/homeassistant/components/synology_dsm/.translations/en.json @@ -1,26 +1,37 @@ { - "config": { - "abort": { - "already_configured": "Host already configured" - }, - "error": { - "login": "Login error: please check your username & password", - "unknown": "Unknown error: please retry later or an other configuration" - }, - "step": { - "user": { - "data": { - "api_version": "DSM version", - "host": "Host", - "name": "Name", - "password": "Password", - "port": "Port", - "ssl": "Use SSL/TLS to connect to your NAS", - "username": "Username" - }, - "title": "Synology DSM" - } - }, - "title": "Synology DSM" + "config": { + "title": "Synology DSM", + "flow_title": "Synology DSM {name} ({host})", + "step": { + "user": { + "title": "Synology DSM", + "data": { + "host": "Host", + "port": "Port (Optional)", + "ssl": "Use SSL/TLS to connect to your NAS", + "api_version": "DSM version", + "username": "Username", + "password": "Password" + } + }, + "link": { + "title": "Synology DSM", + "description": "Do you want to setup {name} ({host})?", + "data": { + "ssl": "Use SSL/TLS to connect to your NAS", + "api_version": "DSM version", + "username": "Username", + "password": "Password", + "port": "Port (Optional)" + } + } + }, + "error": { + "login": "Login error: please check your username & password", + "unknown": "Unknown error: please retry later or an other configuration" + }, + "abort": { + "already_configured": "Host already configured" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e2ada59ec1d..874afb18584 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import ( CONF_API_VERSION, CONF_DISKS, CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, @@ -23,11 +22,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_VOLUMES, DEFAULT_DSM_VERSION, DEFAULT_NAME, DEFAULT_SSL, DOMAIN +from .const import CONF_VOLUMES, DEFAULT_DSM_VERSION, DEFAULT_SSL, DOMAIN CONFIG_SCHEMA = vol.Schema( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index fd23931f13f..f34e01d55bc 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the Synology DSM integration.""" +import logging +from urllib.parse import urlparse + from synology_dsm import SynologyDSM -from synology_dsm.api.core.utilization import SynoCoreUtilization -from synology_dsm.api.dsm.information import SynoDSMInformation -from synology_dsm.api.storage.storage import SynoStorage import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, exceptions +from homeassistant.components import ssdp from homeassistant.const import ( CONF_API_VERSION, CONF_DISKS, @@ -20,13 +21,42 @@ from homeassistant.const import ( from .const import ( CONF_VOLUMES, DEFAULT_DSM_VERSION, - DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SSL, ) from .const import DOMAIN # pylint: disable=unused-import +_LOGGER = logging.getLogger(__name__) + + +def _discovery_schema_with_defaults(discovery_info): + return vol.Schema(_ordered_shared_schema(discovery_info)) + + +def _user_schema_with_defaults(user_input): + user_schema = { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + } + user_schema.update(_ordered_shared_schema(user_input)) + + return vol.Schema(user_schema) + + +def _ordered_shared_schema(schema_input): + return { + vol.Required(CONF_USERNAME, default=schema_input.get(CONF_USERNAME, "")): str, + vol.Required(CONF_PASSWORD, default=schema_input.get(CONF_PASSWORD, "")): str, + vol.Optional(CONF_PORT, default=schema_input.get(CONF_PORT, "")): str, + vol.Optional(CONF_SSL, default=schema_input.get(CONF_SSL, DEFAULT_SSL)): bool, + vol.Optional( + CONF_API_VERSION, + default=schema_input.get(CONF_API_VERSION, DEFAULT_DSM_VERSION), + ): vol.All( + vol.Coerce(int), vol.In([5, 6]), # DSM versions supported by the library + ), + } + class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -34,40 +64,28 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Initialize the synology_dsm config flow.""" + self.discovered_conf = {} + async def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" - - if user_input is None: + if not user_input: user_input = {} + if self.discovered_conf: + user_input.update(self.discovered_conf) + step_id = "link" + data_schema = _discovery_schema_with_defaults(user_input) + else: + step_id = "user" + data_schema = _user_schema_with_defaults(user_input) + return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Optional( - CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) - ): str, - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, - vol.Optional(CONF_PORT, default=user_input.get(CONF_PORT, "")): str, - vol.Optional( - CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL) - ): bool, - vol.Optional( - CONF_API_VERSION, - default=user_input.get(CONF_API_VERSION, DEFAULT_DSM_VERSION), - ): vol.All( - vol.Coerce(int), - vol.In([5, 6]), # DSM versions supported by the library - ), - vol.Required( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") - ): str, - vol.Required( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): str, - } - ), + step_id=step_id, + data_schema=data_schema, errors=errors or {}, + description_placeholders=self.discovered_conf or {}, ) async def async_step_user(self, user_input=None): @@ -77,7 +95,9 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return await self._show_setup_form(user_input, None) - name = user_input.get(CONF_NAME, DEFAULT_NAME) + if self.discovered_conf: + user_input.update(self.discovered_conf) + host = user_input[CONF_HOST] port = user_input.get(CONF_PORT) username = user_input[CONF_USERNAME] @@ -95,35 +115,23 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host, port, username, password, use_ssl, dsm_version=api_version, ) - if not await self.hass.async_add_executor_job(api.login): + try: + serial = await self.hass.async_add_executor_job( + _login_and_fetch_syno_info, api + ) + except InvalidAuth: errors[CONF_USERNAME] = "login" - return await self._show_setup_form(user_input, errors) + except InvalidData: + errors["base"] = "missing_data" - information: SynoDSMInformation = await self.hass.async_add_executor_job( - getattr, api, "information" - ) - utilisation: SynoCoreUtilization = await self.hass.async_add_executor_job( - getattr, api, "utilisation" - ) - storage: SynoStorage = await self.hass.async_add_executor_job( - getattr, api, "storage" - ) - - if ( - information.serial is None - or utilisation.cpu_user_load is None - or storage.disks_ids is None - or storage.volumes_ids is None - ): - errors["base"] = "unknown" + if errors: return await self._show_setup_form(user_input, errors) # Check if already configured - await self.async_set_unique_id(information.serial) + await self.async_set_unique_id(serial) self._abort_if_unique_id_configured() config_data = { - CONF_NAME: name, CONF_HOST: host, CONF_PORT: port, CONF_SSL: use_ssl, @@ -131,12 +139,70 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: password, } if user_input.get(CONF_DISKS): - config_data.update({CONF_DISKS: user_input[CONF_DISKS]}) + config_data[CONF_DISKS] = user_input[CONF_DISKS] if user_input.get(CONF_VOLUMES): - config_data.update({CONF_VOLUMES: user_input[CONF_VOLUMES]}) + config_data[CONF_VOLUMES] = user_input[CONF_VOLUMES] - return self.async_create_entry(title=host, data=config_data,) + return self.async_create_entry(title=host, data=config_data) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered synology_dsm.""" + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + friendly_name = ( + discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() + ) + + if self._host_already_configured(parsed_url.hostname): + return self.async_abort(reason="already_configured") + + self.discovered_conf = { + CONF_NAME: friendly_name, + CONF_HOST: parsed_url.hostname, + } + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = self.discovered_conf + return await self.async_step_user() async def async_step_import(self, user_input=None): """Import a config entry.""" return await self.async_step_user(user_input) + + async def async_step_link(self, user_input=None): + """Link a config entry from discovery.""" + return await self.async_step_user(user_input) + + def _host_already_configured(self, hostname): + """See if we already have a host matching user input configured.""" + existing_hosts = { + entry.data[CONF_HOST] for entry in self._async_current_entries() + } + return hostname in existing_hosts + + +def _login_and_fetch_syno_info(api): + """Login to the NAS and fetch basic data.""" + if not api.login(): + raise InvalidAuth + + # These do i/o + information = api.information + utilisation = api.utilisation + storage = api.storage + + if ( + information.serial is None + or utilisation.cpu_user_load is None + or storage.disks_ids is None + or storage.volumes_ids is None + ): + raise InvalidData + + return information.serial + + +class InvalidData(exceptions.HomeAssistantError): + """Error to indicate we get invalid data from the nas.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 7323413636b..dc58302fa32 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -8,7 +8,7 @@ from homeassistant.const import ( DOMAIN = "synology_dsm" CONF_VOLUMES = "volumes" -DEFAULT_NAME = "Synology DSM" +BASE_NAME = "Synology" DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index a6d171f6528..e2f730ddc8d 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -4,5 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/synology_dsm", "requirements": ["python-synology==0.5.0"], "codeowners": ["@ProtoThis", "@Quentame"], - "config_flow": true + "config_flow": true, + "ssdp": [ + { + "manufacturer": "Synology", + "deviceType": "urn:schemas-upnp-org:device:Basic:1" + } + ] } diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index aeefbc49893..6e5a486ab89 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -5,7 +5,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_DISKS, - CONF_NAME, DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, TEMP_CELSIUS, @@ -16,6 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import SynoApi from .const import ( + BASE_NAME, CONF_VOLUMES, DOMAIN, STORAGE_DISK_SENSORS, @@ -31,12 +31,11 @@ async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up the Synology NAS Sensor.""" - name = entry.data[CONF_NAME] api = hass.data[DOMAIN][entry.unique_id] sensors = [ - SynoNasUtilSensor(api, name, sensor_type, UTILISATION_SENSORS[sensor_type]) + SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) for sensor_type in UTILISATION_SENSORS ] @@ -45,7 +44,7 @@ async def async_setup_entry( for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids): sensors += [ SynoNasStorageSensor( - api, name, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume + api, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume ) for sensor_type in STORAGE_VOL_SENSORS ] @@ -55,7 +54,7 @@ async def async_setup_entry( for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): sensors += [ SynoNasStorageSensor( - api, name, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk + api, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk ) for sensor_type in STORAGE_DISK_SENSORS ] @@ -69,7 +68,6 @@ class SynoNasSensor(Entity): def __init__( self, api: SynoApi, - name: str, sensor_type: str, sensor_info: Dict[str, str], monitored_device: str = None, @@ -77,15 +75,15 @@ class SynoNasSensor(Entity): """Initialize the sensor.""" self._api = api self.sensor_type = sensor_type - self._name = f"{name} {sensor_info[0]}" + self._name = f"{BASE_NAME} {sensor_info[0]}" self._unit = sensor_info[1] self._icon = sensor_info[2] self.monitored_device = monitored_device + self._unique_id = f"{self._api.information.serial}_{sensor_info[0]}" if self.monitored_device: - self._name = f"{self._name} ({self.monitored_device})" - - self._unique_id = f"{self._api.information.serial} {self._name}" + self._name += f" ({self.monitored_device})" + self._unique_id += f"_{self.monitored_device}" self._unsub_dispatcher = None diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index b9ccf8d1010..77bd1250033 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -1,18 +1,29 @@ { "config": { "title": "Synology DSM", + "flow_title": "Synology DSM {name} ({host})", "step": { "user": { "title": "Synology DSM", "data": { - "name": "Name", "host": "Host", - "port": "Port", + "port": "Port (Optional)", "ssl": "Use SSL/TLS to connect to your NAS", "api_version": "DSM version", "username": "Username", "password": "Password" } + }, + "link": { + "title": "Synology DSM", + "description": "Do you want to setup {name} ({host})?", + "data": { + "ssl": "Use SSL/TLS to connect to your NAS", + "api_version": "DSM version", + "username": "Username", + "password": "Password", + "port": "Port (Optional)" + } } }, "error": { diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index c9832ea2d86..4aa8eabe9d9 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -70,6 +70,12 @@ SSDP = { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" } ], + "synology_dsm": [ + { + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "manufacturer": "Synology" + } + ], "wemo": [ { "manufacturer": "Belkin International Inc." diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 664ffda7f8e..27ae28aa50e 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -4,16 +4,16 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, setup +from homeassistant.components import ssdp from homeassistant.components.synology_dsm.const import ( CONF_VOLUMES, - DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PORT_SSL, DEFAULT_SSL, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -47,10 +47,10 @@ def mock_controller_service(): with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM" ) as service_mock: - service_mock.return_value.login = Mock(return_value=True) - service_mock.return_value.information = Mock(serial=SERIAL) - service_mock.return_value.utilisation = Mock(cpu_user_load=1) - service_mock.return_value.storage = Mock(disks_ids=[], volumes_ids=[]) + service_mock.return_value.information.serial = SERIAL + service_mock.return_value.utilisation.cpu_user_load = 1 + service_mock.return_value.storage.disks_ids = [] + service_mock.return_value.storage.volumes_ids = [] yield service_mock @@ -70,10 +70,10 @@ def mock_controller_service_failed(): with patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSM" ) as service_mock: - service_mock.return_value.login = Mock(return_value=True) - service_mock.return_value.information = Mock(serial=None) - service_mock.return_value.utilisation = Mock(cpu_user_load=None) - service_mock.return_value.storage = Mock(disks_ids=None, volumes_ids=None) + service_mock.return_value.information.serial = None + service_mock.return_value.utilisation.cpu_user_load = None + service_mock.return_value.storage.disks_ids = None + service_mock.return_value.storage.volumes_ids = None yield service_mock @@ -90,7 +90,6 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): DOMAIN, context={"source": SOURCE_USER}, data={ - CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT, CONF_SSL: SSL, @@ -101,7 +100,6 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT assert result["data"][CONF_SSL] == SSL @@ -110,7 +108,7 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None - service.return_value.information = Mock(serial=SERIAL_2) + service.return_value.information.serial = SERIAL_2 # test without port + False SSL result = await hass.config_entries.flow.async_init( DOMAIN, @@ -126,7 +124,6 @@ async def test_user(hass: HomeAssistantType, service: MagicMock): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST - assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT assert not result["data"][CONF_SSL] @@ -147,7 +144,6 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST - assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT_SSL assert result["data"][CONF_SSL] == DEFAULT_SSL @@ -156,13 +152,12 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None - service.return_value.information = Mock(serial=SERIAL_2) + service.return_value.information.serial = SERIAL_2 # import with all result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={ - CONF_NAME: NAME, CONF_HOST: HOST_2, CONF_PORT: PORT, CONF_SSL: SSL, @@ -175,7 +170,6 @@ async def test_import(hass: HomeAssistantType, service: MagicMock): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST_2 - assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST_2 assert result["data"][CONF_PORT] == PORT assert result["data"][CONF_SSL] == SSL @@ -223,7 +217,9 @@ async def test_login_failed(hass: HomeAssistantType, service_login_failed: Magic assert result["errors"] == {CONF_USERNAME: "login"} -async def test_connection_failed(hass: HomeAssistantType, service_failed: MagicMock): +async def test_missing_data_after_login( + hass: HomeAssistantType, service_failed: MagicMock +): """Test when we have errors during connection.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -231,4 +227,35 @@ async def test_connection_failed(hass: HomeAssistantType, service_failed: MagicM data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + assert result["errors"] == {"base": "missing_data"} + + +async def test_form_ssdp(hass: HomeAssistantType, service: MagicMock): + """Test we can setup from ssdp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "192.168.1.5" + assert result2["data"] == { + CONF_HOST: "192.168.1.5", + CONF_PORT: DEFAULT_PORT_SSL, + CONF_PASSWORD: PASSWORD, + CONF_SSL: DEFAULT_SSL, + CONF_USERNAME: USERNAME, + }