Add SSDP discovery to unifi (#45364)

pull/45403/head
J. Nick Koston 2021-01-21 11:03:54 -06:00 committed by GitHub
parent ded242a8fe
commit b68c287ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 187 additions and 9 deletions

View File

@ -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."""

View File

@ -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"
}
]
}

View File

@ -1,6 +1,6 @@
{
"config": {
"flow_title": "{site} ({host})",
"flow_title": "UniFi Network {site} ({host})",
"step": {
"user": {
"title": "Set up UniFi Controller",

View File

@ -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": {

View File

@ -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"

View File

@ -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",
}