diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index 00aae5b8a07..0e9c8e96060 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -6,6 +6,7 @@ import logging import aiohttp import python_otbr_api +from python_otbr_api import tlv_parser import voluptuous as vol from homeassistant.components.hassio import HassioServiceInfo @@ -15,7 +16,7 @@ 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 +from .const import DEFAULT_CHANNEL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -29,11 +30,26 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): """Connect to the OTBR and create a dataset if it doesn't have one.""" api = python_otbr_api.OTBR(url, async_get_clientsession(self.hass), 10) if await api.get_active_dataset_tlvs() is None: - if dataset := await async_get_preferred_dataset(self.hass): - await api.set_active_dataset_tlvs(bytes.fromhex(dataset)) + # We currently have no way to know which channel zha is using, assume it's + # the default + zha_channel = DEFAULT_CHANNEL + thread_dataset_channel = None + thread_dataset_tlv = await async_get_preferred_dataset(self.hass) + if thread_dataset_tlv: + dataset = tlv_parser.parse_tlv(thread_dataset_tlv) + if channel_str := dataset.get(tlv_parser.MeshcopTLVType.CHANNEL): + thread_dataset_channel = int(channel_str, base=16) + + if thread_dataset_tlv is not None and zha_channel == thread_dataset_channel: + await api.set_active_dataset_tlvs(bytes.fromhex(thread_dataset_tlv)) else: + _LOGGER.debug( + "not importing TLV with channel %s", thread_dataset_channel + ) await api.create_active_dataset( - python_otbr_api.OperationalDataSet(network_name="home-assistant") + python_otbr_api.OperationalDataSet( + channel=zha_channel, network_name="home-assistant" + ) ) await api.set_enabled(True) diff --git a/homeassistant/components/otbr/const.py b/homeassistant/components/otbr/const.py index 72884a198d8..cc3e4a9e6c3 100644 --- a/homeassistant/components/otbr/const.py +++ b/homeassistant/components/otbr/const.py @@ -1,3 +1,5 @@ """Constants for the Open Thread Border Router integration.""" DOMAIN = "otbr" + +DEFAULT_CHANNEL = 15 diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py index d88581696c4..7c69a8d0a2d 100644 --- a/homeassistant/components/otbr/websocket_api.py +++ b/homeassistant/components/otbr/websocket_api.py @@ -12,7 +12,7 @@ from homeassistant.components.websocket_api import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from .const import DOMAIN +from .const import DEFAULT_CHANNEL, DOMAIN if TYPE_CHECKING: from . import OTBRData @@ -70,6 +70,10 @@ async def websocket_create_network( connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded") return + # We currently have no way to know which channel zha is using, assume it's + # the default + zha_channel = DEFAULT_CHANNEL + data: OTBRData = hass.data[DOMAIN] try: @@ -80,7 +84,9 @@ async def websocket_create_network( try: await data.create_active_dataset( - python_otbr_api.OperationalDataSet(network_name="home-assistant") + python_otbr_api.OperationalDataSet( + channel=zha_channel, network_name="home-assistant" + ) ) except HomeAssistantError as exc: connection.send_error(msg["id"], "create_active_dataset_failed", str(exc)) diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index a133f6fda30..d6b2a406aa1 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1,7 +1,14 @@ """Tests for the Open Thread Border Router integration.""" BASE_URL = "http://core-silabs-multiprotocol:8081" CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} -DATASET = bytes.fromhex( + +DATASET_CH15 = bytes.fromhex( + "0E080000000000010000000300000F35060004001FFFE00208F642646DA209B1C00708FDF57B5A" + "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" + "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" +) + +DATASET_CH16 = bytes.fromhex( "0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index ac120b3e164..368ecfe8095 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -5,7 +5,7 @@ import pytest from homeassistant.components import otbr -from . import CONFIG_ENTRY_DATA, DATASET +from . import CONFIG_ENTRY_DATA, DATASET_CH16 from tests.common import MockConfigEntry @@ -21,7 +21,7 @@ async def otbr_config_entry_fixture(hass): ) config_entry.add_to_hass(hass) with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ), patch( "homeassistant.components.otbr.compute_pskc" ): # Patch to speed up tests diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index e27cfb219cf..2ec79dcaeed 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -11,6 +11,8 @@ from homeassistant.components import hassio, otbr from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import DATASET_CH15, DATASET_CH16 + from tests.common import MockConfigEntry, MockModule, mock_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -94,7 +96,10 @@ async def test_user_flow_router_not_setup( # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "POST" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" - assert aioclient_mock.mock_calls[-2][2] == {"NetworkName": "home-assistant"} + assert aioclient_mock.mock_calls[-2][2] == { + "Channel": 15, + "NetworkName": "home-assistant", + } assert aioclient_mock.mock_calls[-1][0] == "POST" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" @@ -226,7 +231,10 @@ async def test_hassio_discovery_flow_router_not_setup( # Check we create a dataset and enable the router assert aioclient_mock.mock_calls[-2][0] == "POST" assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" - assert aioclient_mock.mock_calls[-2][2] == {"NetworkName": "home-assistant"} + assert aioclient_mock.mock_calls[-2][2] == { + "Channel": 15, + "NetworkName": "home-assistant", + } assert aioclient_mock.mock_calls[-1][0] == "POST" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" @@ -263,7 +271,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( with patch( "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", - return_value="aa", + return_value=DATASET_CH15.hex(), ), patch( "homeassistant.components.otbr.async_setup_entry", return_value=True, @@ -275,7 +283,60 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( # 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] == "aa" + assert aioclient_mock.mock_calls[-2][2] == DATASET_CH15.hex() + + assert aioclient_mock.mock_calls[-1][0] == "POST" + 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"] == 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 == otbr.DOMAIN + + +async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> 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.post(f"{url}/node/dataset/active", status=HTTPStatus.ACCEPTED) + aioclient_mock.post(f"{url}/node/state", status=HTTPStatus.OK) + + 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] == "POST" + assert aioclient_mock.mock_calls[-2][1].path == "/node/dataset/active" + assert aioclient_mock.mock_calls[-2][2] == { + "Channel": 15, + "NetworkName": "home-assistant", + } assert aioclient_mock.mock_calls[-1][0] == "POST" assert aioclient_mock.mock_calls[-1][1].path == "/node/state" diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 9261004ec1c..86443ce5c0c 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -15,7 +15,7 @@ from homeassistant.helpers import issue_registry as ir from . import ( BASE_URL, CONFIG_ENTRY_DATA, - DATASET, + DATASET_CH16, DATASET_INSECURE_NW_KEY, DATASET_INSECURE_PASSPHRASE, ) @@ -36,13 +36,13 @@ async def test_import_dataset(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 ), patch( "homeassistant.components.thread.dataset_store.DatasetStore.async_add" ) as mock_add: assert await hass.config_entries.async_setup(config_entry.entry_id) - mock_add.assert_called_once_with(config_entry.title, DATASET.hex()) + mock_add.assert_called_once_with(config_entry.title, DATASET_CH16.hex()) assert not issue_registry.async_get_issue( domain=otbr.DOMAIN, issue_id=f"insecure_thread_network_{config_entry.entry_id}" ) diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index de01c6153e2..78935657431 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -124,7 +124,9 @@ async def test_create_network( assert msg["result"] is None create_dataset_mock.assert_called_once_with( - python_otbr_api.models.OperationalDataSet(network_name="home-assistant") + python_otbr_api.models.OperationalDataSet( + channel=15, network_name="home-assistant" + ) ) assert len(set_enabled_mock.mock_calls) == 2 assert set_enabled_mock.mock_calls[0][1][0] is False