diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 6d266025946..85203204b2f 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,9 +1,11 @@ """Config flow for UniFi.""" import socket +from urllib.parse import urlparse import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -42,6 +45,12 @@ DEFAULT_SITE_ID = "default" DEFAULT_VERIFY_SSL = False +MODEL_PORTS = { + "UniFi Dream Machine": 443, + "UniFi Dream Machine Pro": 443, +} + + @callback def get_controller_id_from_config_entry(config_entry): """Return controller with a matching bridge id.""" @@ -65,7 +74,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self): """Initialize the UniFi flow.""" - self.config = None + self.config = {} self.sites = None self.reauth_config_entry = {} self.reauth_config = {} @@ -112,15 +121,17 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): ) return self.async_abort(reason="unknown") - host = "" - if await async_discover_unifi(self.hass): + host = self.config.get(CONF_HOST) + if not host and await async_discover_unifi(self.hass): host = "unifi" data = self.reauth_schema or { vol.Required(CONF_HOST, default=host): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional( + CONF_PORT, default=self.config.get(CONF_PORT, DEFAULT_PORT) + ): int, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, } @@ -194,6 +205,43 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() + async def async_step_ssdp(self, discovery_info): + """Handle a discovered unifi device.""" + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] + mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL]) + + self.config = { + CONF_HOST: parsed_url.hostname, + } + + if self._host_already_configured(self.config[CONF_HOST]): + return self.async_abort(reason="already_configured") + + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.config[CONF_HOST]}) + + # pylint: disable=no-member + self.context["title_placeholders"] = { + CONF_HOST: self.config[CONF_HOST], + CONF_SITE_ID: "default", + } + + port = MODEL_PORTS.get(model_description) + if port is not None: + self.config[CONF_PORT] = port + + return await self.async_step_user() + + def _host_already_configured(self, host): + """See if we already have a unifi entry matching the host.""" + for entry in self._async_current_entries(): + if not entry.data: + continue + if entry.data[CONF_CONTROLLER][CONF_HOST] == host: + return True + return False + class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi options.""" diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 94b1c90f4f3..cec2d0f859b 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -5,5 +5,17 @@ "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "ssdp": [ + { + "manufacturer": "Ubiquiti Networks", + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "modelDescription": "UniFi Dream Machine" + }, + { + "manufacturer": "Ubiquiti Networks", + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "modelDescription": "UniFi Dream Machine Pro" + } + ] } diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 1bfba247ddc..15cc2fb45e7 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "{site} ({host})", + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "title": "Set up UniFi Controller", diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index d5da167c1b1..06e8ae1eb60 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Controller site is already configured", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "Re-authentication was successful" }, "error": { "faulty_credentials": "Invalid authentication", "service_unavailable": "Failed to connect", "unknown_client_mac": "No client available on that MAC address" }, - "flow_title": "{site} ({host})", + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1617cd35435..e17a20bfa7b 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -166,6 +166,18 @@ SSDP = { "manufacturer": "Synology" } ], + "unifi": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine" + }, + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro" + } + ], "upnp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 0626d8d7b86..6eb049b573a 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiounifi -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, @@ -466,3 +466,109 @@ async def test_simple_option_flow(hass): CONF_TRACK_DEVICES: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], } + + +async def test_form_ssdp(hass): + """Test we get the form with ssdp source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"] == { + "host": "192.168.208.1", + "site": "default", + } + + +async def test_form_ssdp_aborts_if_host_already_exists(hass): + """Test we abort if the host is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={"controller": {"host": "192.168.208.1", "site": "site_id"}}, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_ssdp_aborts_if_serial_already_exists(hass): + """Test we abort if the serial is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, + unique_id="e0:63:da:20:14:a9", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_ssdp_gets_form_with_ignored_entry(hass): + """Test we can still setup if there is an ignored entry.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine New", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://1.2.3.4:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"] == { + "host": "1.2.3.4", + "site": "default", + }