diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 9af74cb4423..f065f1c27ef 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -7,6 +7,7 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from .const import ( # pylint: disable=unused-import @@ -54,6 +55,10 @@ async def validate_input(hass, data): class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Nuki config flow.""" + def __init__(self): + """Initialize the Nuki config flow.""" + self.discovery_schema = {} + async def async_step_import(self, user_input=None): """Handle a flow initiated by import.""" return await self.async_step_validate(user_input) @@ -62,7 +67,23 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" return await self.async_step_validate(user_input) - async def async_step_validate(self, user_input): + async def async_step_dhcp(self, discovery_info: dict): + """Prepare configuration for a DHCP discovered Nuki bridge.""" + await self.async_set_unique_id(int(discovery_info.get(HOSTNAME)[12:], 16)) + + self._abort_if_unique_id_configured() + + self.discovery_schema = vol.Schema( + { + vol.Required(CONF_HOST, default=discovery_info[IP_ADDRESS]): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_TOKEN): str, + } + ) + + return await self.async_step_validate() + + async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" errors = {} @@ -84,8 +105,10 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info["ids"]["hardwareId"], data=user_input ) + data_schema = self.discovery_schema or USER_SCHEMA + return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors + step_id="user", data_schema=data_schema, errors=errors ) diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 9385821845a..7fb9a134c4c 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nuki", "requirements": ["pynuki==1.3.8"], "codeowners": ["@pschmitt", "@pvizeli", "@pree"], - "config_flow": true + "config_flow": true, + "dhcp": [{ "hostname": "nuki_bridge_*" }] } \ No newline at end of file diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 61223bf00f7..31ee42bc48c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -70,6 +70,10 @@ DHCP = [ "hostname": "nuheat", "macaddress": "002338*" }, + { + "domain": "nuki", + "hostname": "nuki_bridge_*" + }, { "domain": "powerwall", "hostname": "1118431-*", diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py new file mode 100644 index 00000000000..a7870ce0906 --- /dev/null +++ b/tests/components/nuki/mock.py @@ -0,0 +1,25 @@ +"""Mockup Nuki device.""" +from homeassistant import setup + +from tests.common import MockConfigEntry + +NAME = "Nuki_Bridge_75BCD15" +HOST = "1.1.1.1" +MAC = "01:23:45:67:89:ab" + +HW_ID = 123456789 + +MOCK_INFO = {"ids": {"hardwareId": HW_ID}} + + +async def setup_nuki_integration(hass): + """Create the Nuki device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="nuki", + unique_id=HW_ID, + data={"host": HOST, "port": 8080, "token": "test-token"}, + ) + entry.add_to_hass(hass) + + return entry diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index bcdedad371a..4933ea52b77 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -5,9 +5,10 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.nuki.const import DOMAIN -from tests.common import MockConfigEntry +from .mock import HOST, MAC, MOCK_INFO, NAME, setup_nuki_integration async def test_form(hass): @@ -19,11 +20,9 @@ async def test_form(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - mock_info = {"ids": {"hardwareId": "0001"}} - with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=mock_info, + return_value=MOCK_INFO, ), patch( "homeassistant.components.nuki.async_setup", return_value=True ) as mock_setup, patch( @@ -41,7 +40,7 @@ async def test_form(hass): await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "0001" + assert result2["title"] == 123456789 assert result2["data"] == { "host": "1.1.1.1", "port": 8080, @@ -55,11 +54,9 @@ async def test_import(hass): """Test that the import works.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_info = {"ids": {"hardwareId": "0001"}} - with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=mock_info, + return_value=MOCK_INFO, ), patch( "homeassistant.components.nuki.async_setup", return_value=True ) as mock_setup, patch( @@ -72,7 +69,7 @@ async def test_import(hass): data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "0001" + assert result["title"] == 123456789 assert result["data"] == { "host": "1.1.1.1", "port": 8080, @@ -155,21 +152,14 @@ async def test_form_unknown_exception(hass): async def test_form_already_configured(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - entry = MockConfigEntry( - domain="nuki", - unique_id="0001", - data={"host": "1.1.1.1", "port": 8080, "token": "test-token"}, - ) - entry.add_to_hass(hass) - + await setup_nuki_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value={"ids": {"hardwareId": "0001"}}, + return_value=MOCK_INFO, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -182,3 +172,58 @@ async def test_form_already_configured(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" + + +async def test_dhcp_flow(hass): + """Test that DHCP discovery for new bridge works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), patch( + "homeassistant.components.nuki.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == 123456789 + assert result2["data"] == { + "host": "1.1.1.1", + "port": 8080, + "token": "test-token", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_flow_already_configured(hass): + """Test that DHCP doesn't setup already configured devices.""" + await setup_nuki_integration(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={HOSTNAME: NAME, IP_ADDRESS: HOST, MAC_ADDRESS: MAC}, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured"