"""Test the Open Thread Border Router config flow.""" import asyncio from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, Mock, patch import aiohttp import pytest import python_otbr_api from homeassistant.components import otbr from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo from . import DATASET_CH15, DATASET_CH16, TEST_BORDER_AGENT_ID, TEST_BORDER_AGENT_ID_2 from tests.common import MockConfigEntry, MockModule, mock_integration from tests.test_util.aiohttp import AiohttpClientMocker HASSIO_DATA = HassioServiceInfo( config={"host": "core-silabs-multiprotocol", "port": 8081}, name="Silicon Labs Multiprotocol", slug="otbr", uuid="12345", ) HASSIO_DATA_2 = HassioServiceInfo( config={"host": "core-silabs-multiprotocol_2", "port": 8082}, name="Silicon Labs Multiprotocol", slug="other_addon", uuid="23456", ) @pytest.fixture(name="otbr_addon_info") def otbr_addon_info_fixture(addon_info: AsyncMock, addon_installed) -> AsyncMock: """Mock Supervisor otbr add-on info.""" addon_info.return_value.available = True addon_info.return_value.hostname = "" addon_info.return_value.options = {} addon_info.return_value.state = "unknown" addon_info.return_value.update_available = False addon_info.return_value.version = None return addon_info @pytest.mark.parametrize( "url", [ "http://custom_url:1234", "http://custom_url:1234/", "http://custom_url:1234//", ], ) @pytest.mark.usefixtures( "get_active_dataset_tlvs", "get_border_agent_id", ) async def test_user_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, url: str ) -> None: """Test the user flow.""" await _finish_user_flow(hass, url) @pytest.mark.usefixtures( "get_active_dataset_tlvs", "get_extended_address", ) async def test_user_flow_additional_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test more than a single entry is allowed.""" url1 = "http://custom_url:1234" url2 = "http://custom_url_2:1234" aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID_2.hex()) mock_integration(hass, MockModule("hassio")) # Setup a config entry config_entry = MockConfigEntry( data={"url": url2}, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", unique_id=TEST_BORDER_AGENT_ID_2.hex(), ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) # Do a user flow await _finish_user_flow(hass) @pytest.mark.usefixtures( "get_active_dataset_tlvs", "get_extended_address", ) async def test_user_flow_additional_entry_fail_get_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: """Test more than a single entry is allowed. This tets the behavior when we can't read the extended address from the existing config entry. """ url1 = "http://custom_url:1234" url2 = "http://custom_url_2:1234" aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID_2.hex()) mock_integration(hass, MockModule("hassio")) # Setup a config entry config_entry = MockConfigEntry( data={"url": url2}, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", unique_id=TEST_BORDER_AGENT_ID_2.hex(), ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) # Do a user flow aioclient_mock.clear_requests() aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", status=HTTPStatus.NOT_FOUND) await _finish_user_flow(hass) assert f"Could not read border agent id from {url2}" in caplog.text async def _finish_user_flow( hass: HomeAssistant, url: str = "http://custom_url:1234" ) -> None: """Finish a user flow.""" stripped_url = "http://custom_url:1234" result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) expected_data = {"url": stripped_url} assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], { "url": url, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = result["result"] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Open Thread Border Router" assert config_entry.unique_id == TEST_BORDER_AGENT_ID.hex() @pytest.mark.usefixtures( "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", ) async def test_user_flow_additional_entry_same_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test more than a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) # Setup a config entry config_entry = MockConfigEntry( data={"url": "http://custom_url:1234"}, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", unique_id=TEST_BORDER_AGENT_ID.hex(), ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) # Start 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"} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { "url": url, }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "already_configured"} @pytest.mark.usefixtures("get_border_agent_id") async def test_user_flow_router_not_setup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the user flow when the border router has no dataset. This tests the behavior when the thread integration has no preferred dataset. """ url = "http://custom_url:1234" aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NO_CONTENT) aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with ( patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", return_value=None, ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { "url": url, }, ) # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, "NetworkName": f"ha-thread-{pan_id:04x}", "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" assert aioclient_mock.mock_calls[-1][2] == "enable" expected_data = { "url": "http://custom_url:1234", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Open Thread Border Router" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Open Thread Border Router" assert config_entry.unique_id == TEST_BORDER_AGENT_ID.hex() @pytest.mark.usefixtures("get_border_agent_id") async def test_user_flow_get_dataset_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"] is FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { "url": url, }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @pytest.mark.parametrize( "error", [ TimeoutError, python_otbr_api.OTBRError, aiohttp.ClientError, ], ) async def test_user_flow_get_ba_id_connect_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, error ) -> None: """Test the user flow.""" await _test_user_flow_connect_error(hass, "get_border_agent_id", error) @pytest.mark.usefixtures("get_border_agent_id") @pytest.mark.parametrize( "error", [ TimeoutError, python_otbr_api.OTBRError, aiohttp.ClientError, ], ) async def test_user_flow_get_dataset_connect_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, error ) -> None: """Test the user flow.""" await _test_user_flow_connect_error(hass, "get_active_dataset_tlvs", error) async def _test_user_flow_connect_error(hass: HomeAssistant, func, error) -> None: """Test the user flow.""" result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "user"} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} with patch(f"python_otbr_api.OTBR.{func}", side_effect=error): result = await hass.config_entries.flow.async_configure( result["flow_id"], { "url": "http://custom_url:1234", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", text="aa") with patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Silicon Labs Multiprotocol" assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_yellow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", text="aa") otbr_addon_info.return_value.available = True otbr_addon_info.return_value.options = {"device": "/dev/ttyAMA1"} with ( patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry, patch("homeassistant.components.otbr.config_flow.yellow_hardware.async_info"), ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Home Assistant Yellow (Silicon Labs Multiprotocol)" assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.parametrize( ("device", "title"), [ ( "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", "Home Assistant SkyConnect (Silicon Labs Multiprotocol)", ), ( "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", "Home Assistant Connect ZBT-1 (Silicon Labs Multiprotocol)", ), ], ) @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_sky_connect( device: str, title: str, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info, ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", text="aa") otbr_addon_info.return_value.available = True otbr_addon_info.return_value.options = {"device": device} with patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == title assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_active_dataset_tlvs", "get_extended_address") async def test_hassio_discovery_flow_2x_addons( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the user has 2 addons with otbr support.""" url1 = "http://core-silabs-multiprotocol:8081" url2 = "http://core-silabs-multiprotocol_2:8081" aioclient_mock.get(f"{url1}/node/dataset/active", text="aa") aioclient_mock.get(f"{url2}/node/dataset/active", text="bb") aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID_2.hex()) async def _addon_info(slug: str) -> Mock: await asyncio.sleep(0) if slug == "otbr": device = ( "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" "9e2adbd75b8beb119fe564a0f320645d-if00-port0" ) else: device = ( "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" "9e2adbd75b8beb119fe564a0f320645d-if00-port1" ) return Mock( available=True, hostname=otbr_addon_info.return_value.hostname, options={"device": device}, state=otbr_addon_info.return_value.state, update_available=otbr_addon_info.return_value.update_available, version=otbr_addon_info.return_value.version, ) otbr_addon_info.side_effect = _addon_info result1 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) result2 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2 ) results = [result1, result2] expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } expected_data_2 = { "url": f"http://{HASSIO_DATA_2.config['host']}:{HASSIO_DATA_2.config['port']}", } assert results[0]["type"] is FlowResultType.CREATE_ENTRY assert ( results[0]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert results[0]["data"] == expected_data assert results[0]["options"] == {} assert results[1]["type"] is FlowResultType.CREATE_ENTRY assert ( results[1]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert results[1]["data"] == expected_data_2 assert results[1]["options"] == {} assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 2 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert ( config_entry.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert config_entry.unique_id == HASSIO_DATA.uuid config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[1] assert config_entry.data == expected_data_2 assert config_entry.options == {} assert ( config_entry.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert config_entry.unique_id == HASSIO_DATA_2.uuid @pytest.mark.usefixtures("get_active_dataset_tlvs", "get_extended_address") async def test_hassio_discovery_flow_2x_addons_same_ext_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the user has 2 addons with otbr support.""" url1 = "http://core-silabs-multiprotocol:8081" url2 = "http://core-silabs-multiprotocol_2:8081" aioclient_mock.get(f"{url1}/node/dataset/active", text="aa") aioclient_mock.get(f"{url2}/node/dataset/active", text="bb") aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) async def _addon_info(slug: str) -> Mock: await asyncio.sleep(0) if slug == "otbr": device = ( "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" "9e2adbd75b8beb119fe564a0f320645d-if00-port0" ) else: device = ( "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" "9e2adbd75b8beb119fe564a0f320645d-if00-port1" ) return Mock( available=True, hostname=otbr_addon_info.return_value.hostname, options={"device": device}, state=otbr_addon_info.return_value.state, update_available=otbr_addon_info.return_value.update_available, version=otbr_addon_info.return_value.version, ) otbr_addon_info.side_effect = _addon_info result1 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) result2 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_2 ) results = [result1, result2] expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert results[0]["type"] is FlowResultType.CREATE_ENTRY assert ( results[0]["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert results[0]["data"] == expected_data assert results[0]["options"] == {} assert results[1]["type"] is FlowResultType.ABORT assert results[1]["reason"] == "already_configured" assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert ( config_entry.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" ) assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_router_not_setup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the border router has no dataset. This tests the behavior when the thread integration has no preferred dataset. """ url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NO_CONTENT) aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) with ( patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", return_value=None, ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, "NetworkName": f"ha-thread-{pan_id:04x}", "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" assert aioclient_mock.mock_calls[-1][2] == "enable" expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Silicon Labs Multiprotocol" assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_router_not_setup_has_preferred( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the border router has no dataset. This tests the behavior when the thread integration has a preferred dataset. """ url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NO_CONTENT) aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) with ( patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", return_value=DATASET_CH15.hex(), ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" assert aioclient_mock.mock_calls[-2][2] == DATASET_CH15.hex() assert aioclient_mock.mock_calls[-1][0] == "PUT" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" assert aioclient_mock.mock_calls[-1][2] == "enable" expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Silicon Labs Multiprotocol" assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, multiprotocol_addon_manager_mock, otbr_addon_info, ) -> None: """Test the hassio discovery flow when the border router has no dataset. This tests the behavior when the thread integration has a preferred dataset, but the preferred dataset is not using channel 15. """ url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NO_CONTENT) aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 with ( patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", return_value=DATASET_CH16.hex(), ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "PUT" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" pan_id = aioclient_mock.mock_calls[-2][2]["PanId"] assert aioclient_mock.mock_calls[-2][2] == { "Channel": 15, "NetworkName": f"ha-thread-{pan_id:04x}", "PanId": pan_id, } assert aioclient_mock.mock_calls[-1][0] == "PUT" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" assert aioclient_mock.mock_calls[-1][2] == "enable" expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Silicon Labs Multiprotocol" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} assert config_entry.title == "Silicon Labs Multiprotocol" assert config_entry.unique_id == HASSIO_DATA.uuid @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_404( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the user and discovery flows.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NOT_FOUND) result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unknown" @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_new_port_missing_unique_id( hass: HomeAssistant, ) -> None: """Test the port can be updated when the unique id is missing.""" mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( data={ "url": ( f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port'] + 1}" ) }, domain=otbr.DOMAIN, options={}, source="hassio", title="Open Thread Border Router", ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None: """Test the port can be updated.""" mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( data={ "url": ( f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port'] + 1}" ) }, domain=otbr.DOMAIN, options={}, source="hassio", title="Open Thread Border Router", unique_id=HASSIO_DATA.uuid, ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" expected_data = { "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", } config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data @pytest.mark.usefixtures( "otbr_addon_info", "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", ) async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) -> None: """Test the port is not updated if we get data for another addon hosting OTBR.""" mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( data={ "url": f"http://openthread_border_router:{HASSIO_DATA.config['port'] + 1}" }, domain=otbr.DOMAIN, options={}, source="hassio", title="Open Thread Border Router", ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) # Another entry will be created assert result["type"] is FlowResultType.CREATE_ENTRY # Make sure the data of the existing entry was not updated expected_data = { "url": f"http://openthread_border_router:{HASSIO_DATA.config['port'] + 1}", } config_entry = hass.config_entries.async_get_entry(config_entry.entry_id) assert config_entry.data == expected_data @pytest.mark.parametrize( ("source", "data", "expected_result"), [ ("hassio", HASSIO_DATA, FlowResultType.CREATE_ENTRY), ("user", None, FlowResultType.FORM), ], ) @pytest.mark.usefixtures( "otbr_addon_info", "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", ) async def test_config_flow_additional_entry( hass: HomeAssistant, source: str, data: Any, expected_result: FlowResultType ) -> None: """Test more than a single entry is allowed.""" mock_integration(hass, MockModule("hassio")) # Setup the config entry config_entry = MockConfigEntry( data={}, domain=otbr.DOMAIN, options={}, title="Open Thread Border Router", ) config_entry.add_to_hass(hass) with patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": source}, data=data ) assert result["type"] is expected_result