diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 56daba335b6..812ee15da2f 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -1,12 +1,14 @@ """Config flow for the Open Thread Border Router integration.""" from __future__ import annotations +import python_otbr_api import voluptuous as vol from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_URL from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,14 +25,26 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + errors = {} + if user_input is not None: - return self.async_create_entry( - title="Thread", - data={"url": user_input[CONF_URL]}, - ) + url = user_input[CONF_URL] + api = python_otbr_api.OTBR(url, async_get_clientsession(self.hass), 10) + try: + await api.get_active_dataset_tlvs() + except python_otbr_api.OTBRError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(DOMAIN) + return self.async_create_entry( + title="Thread", + data=user_input, + ) data_schema = vol.Schema({CONF_URL: str}) - return self.async_show_form(step_id="user", data_schema=data_schema) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle hassio discovery.""" @@ -38,7 +52,9 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="single_instance_allowed") config = discovery_info.config + url = f"http://{config['host']}:{config['port']}" + await self.async_set_unique_id(DOMAIN) return self.async_create_entry( title="Thread", - data={"url": f"http://{config['host']}:{config['port']}"}, + data={"url": url}, ) diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json index 9779ce74d94..989a0f54b76 100644 --- a/homeassistant/components/otbr/manifest.json +++ b/homeassistant/components/otbr/manifest.json @@ -3,7 +3,7 @@ "after_dependencies": ["hassio"], "domain": "otbr", "iot_class": "local_polling", - "config_flow": false, + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/otbr", "integration_type": "system", "name": "Thread", diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json index 76823df4f89..58b32276ba8 100644 --- a/homeassistant/components/otbr/strings.json +++ b/homeassistant/components/otbr/strings.json @@ -8,6 +8,9 @@ "description": "Provide URL for the Open Thread Border Router's REST API" } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } diff --git a/homeassistant/components/otbr/translations/en.json b/homeassistant/components/otbr/translations/en.json new file mode 100644 index 00000000000..36101b77bea --- /dev/null +++ b/homeassistant/components/otbr/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Provide URL for the Open Thread Border Router's REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ca4ca2e2c4f..d55e1eee4f6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -304,6 +304,7 @@ FLOWS = { "openuv", "openweathermap", "oralb", + "otbr", "overkiz", "ovo_energy", "owntracks", diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 2580ab2e24a..ab1200f9a14 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -1,11 +1,15 @@ """Test the Open Thread Border Router config flow.""" +from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components import hassio, otbr from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.test_util.aiohttp import AiohttpClientMocker HASSIO_DATA = hassio.HassioServiceInfo( config={"host": "blah", "port": "bluh"}, @@ -14,16 +18,20 @@ HASSIO_DATA = hassio.HassioServiceInfo( ) -async def test_user_flow(hass: HomeAssistant) -> None: +async def test_user_flow( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", text="aa") result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) - expected_data = {"url": "http://custom_url:1234"} + expected_data = {"url": url} assert result["type"] == FlowResultType.FORM - assert result["errors"] is None + assert result["errors"] == {} with patch( "homeassistant.components.otbr.async_setup_entry", @@ -32,7 +40,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "url": "http://custom_url:1234", + "url": url, }, ) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -45,7 +53,30 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Thread" - assert config_entry.unique_id is None + assert config_entry.unique_id == otbr.DOMAIN + + +async def test_user_flow_404( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NOT_FOUND) + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": url, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: @@ -72,10 +103,11 @@ async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Thread" - assert config_entry.unique_id is None + assert config_entry.unique_id == otbr.DOMAIN -async def test_config_flow_single_entry(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("source", ("hassio", "user")) +async def test_config_flow_single_entry(hass: HomeAssistant, source: str) -> None: """Test only a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) @@ -93,7 +125,7 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + otbr.DOMAIN, context={"source": source} ) assert result["type"] == FlowResultType.ABORT