Only allow channel 15 during configuration of OTBR (#88874)

* Only allow channel 15 during automatic configuration of OTBR

* Also force channel 15 when creating a new network
This commit is contained in:
Erik Montnemery 2023-02-28 17:08:45 +01:00 committed by GitHub
parent e74613f8be
commit f93bd8ef2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 111 additions and 17 deletions

View File

@ -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)

View File

@ -1,3 +1,5 @@
"""Constants for the Open Thread Border Router integration."""
DOMAIN = "otbr"
DEFAULT_CHANNEL = 15

View File

@ -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))

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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}"
)

View File

@ -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