diff --git a/homeassistant/components/thread/config_flow.py b/homeassistant/components/thread/config_flow.py index 978b4c10779f..c6f151b8e63d 100644 --- a/homeassistant/components/thread/config_flow.py +++ b/homeassistant/components/thread/config_flow.py @@ -1,6 +1,7 @@ """Config flow for the Thread integration.""" from __future__ import annotations +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.data_entry_flow import FlowResult @@ -12,8 +13,16 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Set up because the user has border routers.""" + await self._async_handle_discovery_without_unique_id() + return self.async_create_entry(title="Thread", data={}) + async def async_step_import( self, import_data: dict[str, str] | None = None ) -> FlowResult: """Set up by import from async_setup.""" + await self._async_handle_discovery_without_unique_id() return self.async_create_entry(title="Thread", data={}) diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index a6e823de570d..144a10b878e8 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -7,5 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/thread", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["python-otbr-api==1.0.3"] + "requirements": ["python-otbr-api==1.0.3"], + "zeroconf": ["_meshcop._udp.local."] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ae9668f37298..e00a0710c339 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -522,6 +522,11 @@ ZEROCONF = { "domain": "apple_tv", }, ], + "_meshcop._udp.local.": [ + { + "domain": "thread", + }, + ], "_miio._udp.local.": [ { "domain": "xiaomi_aqara", diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index 4b27144166b2..5f19f233e3f6 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -1,10 +1,34 @@ """Test the Thread config flow.""" from unittest.mock import patch -from homeassistant.components import thread +from homeassistant.components import thread, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +TEST_ZEROCONF_RECORD = zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + hostname="HomeAssistant OpenThreadBorderRouter #0BBF", + name="HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", + addresses=["127.0.0.1"], + port=8080, + properties={ + "rv": "1", + "vn": "HomeAssistant", + "mn": "OpenThreadBorderRouter", + "nn": "OpenThread HC", + "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", + "tv": "1.3.0", + "xa": "\xae\xeb/YKW\x0b\xbf", + "sb": "\x00\x00\x01\xb1", + "at": "\x00\x00\x00\x00\x00\x01\x00\x00", + "pt": "\x8f\x06Q~", + "sq": "3", + "bb": "\xf0\xbf", + "dn": "DefaultDomain", + }, + type="_meshcop._udp.local.", +) + async def test_import(hass: HomeAssistant) -> None: """Test the import flow.""" @@ -27,3 +51,76 @@ async def test_import(hass: HomeAssistant) -> None: assert config_entry.options == {} assert config_entry.title == "Thread" assert config_entry.unique_id is None + + +async def test_import_then_zeroconf(hass: HomeAssistant) -> None: + """Test the import flow.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "import"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_zeroconf(hass: HomeAssistant) -> None: + """Test the zeroconf flow.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Thread" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Thread" + assert config_entry.unique_id is None + + +async def test_zeroconf_then_import(hass: HomeAssistant) -> None: + """Test the import flow.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "import"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0