Skip to content
Snippets Groups Projects
Unverified Commit e2d84f9a authored by Erik Montnemery's avatar Erik Montnemery Committed by GitHub
Browse files

Add support for multiple otbr config entries (#124289)

* Add support for multiple otbr config entries

* Fix test

* Drop useless fixture

* Address review comments

* Change unique id from xa to id

* Improve error text

* Store data in ConfigEntry.runtime_data

* Remove useless function
parent 52b6f003
No related branches found
No related tags found
No related merge requests found
Showing
with 565 additions and 176 deletions
......@@ -2,6 +2,8 @@
from __future__ import annotations
import logging
import aiohttp
import python_otbr_api
......@@ -14,22 +16,28 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import DATA_OTBR, DOMAIN
from .util import OTBRData, update_issues
from .const import DOMAIN
from .util import (
GetBorderAgentIdNotSupported,
OTBRData,
update_issues,
update_unique_id,
)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
type OTBRConfigEntry = ConfigEntry[OTBRData]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Open Thread Border Router component."""
websocket_api.async_setup(hass)
if len(config_entries := hass.config_entries.async_entries(DOMAIN)):
for config_entry in config_entries[1:]:
await hass.config_entries.async_remove(config_entry.entry_id)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: OTBRConfigEntry) -> bool:
"""Set up an Open Thread Border Router config entry."""
api = python_otbr_api.OTBR(entry.data["url"], async_get_clientsession(hass), 10)
......@@ -38,13 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
border_agent_id = await otbrdata.get_border_agent_id()
dataset_tlvs = await otbrdata.get_active_dataset_tlvs()
extended_address = await otbrdata.get_extended_address()
except (
HomeAssistantError,
aiohttp.ClientError,
TimeoutError,
) as err:
raise ConfigEntryNotReady("Unable to connect") from err
if border_agent_id is None:
except GetBorderAgentIdNotSupported:
ir.async_create_issue(
hass,
DOMAIN,
......@@ -55,6 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="get_get_border_agent_id_unsupported",
)
return False
except (
HomeAssistantError,
aiohttp.ClientError,
TimeoutError,
) as err:
raise ConfigEntryNotReady("Unable to connect") from err
await update_unique_id(hass, entry, border_agent_id)
if dataset_tlvs:
await update_issues(hass, otbrdata, dataset_tlvs)
await async_add_dataset(
......@@ -66,18 +75,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
hass.data[DATA_OTBR] = otbrdata
entry.runtime_data = otbrdata
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: OTBRConfigEntry) -> bool:
"""Unload a config entry."""
hass.data.pop(DATA_OTBR)
return True
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_reload_entry(hass: HomeAssistant, entry: OTBRConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id)
......@@ -4,7 +4,7 @@ from __future__ import annotations
from contextlib import suppress
import logging
from typing import cast
from typing import TYPE_CHECKING, cast
import aiohttp
import python_otbr_api
......@@ -33,9 +33,16 @@ from .util import (
get_allowed_channel,
)
if TYPE_CHECKING:
from . import OTBRConfigEntry
_LOGGER = logging.getLogger(__name__)
class AlreadyConfigured(HomeAssistantError):
"""Raised when the router is already configured."""
def _is_yellow(hass: HomeAssistant) -> bool:
"""Return True if Home Assistant is running on a Home Assistant Yellow."""
try:
......@@ -70,9 +77,8 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def _connect_and_set_dataset(self, otbr_url: str) -> None:
async def _set_dataset(self, api: python_otbr_api.OTBR, otbr_url: str) -> None:
"""Connect to the OTBR and create or apply a dataset if it doesn't have one."""
api = python_otbr_api.OTBR(otbr_url, async_get_clientsession(self.hass), 10)
if await api.get_active_dataset_tlvs() is None:
allowed_channel = await get_allowed_channel(self.hass, otbr_url)
......@@ -89,7 +95,9 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
await api.set_active_dataset_tlvs(bytes.fromhex(thread_dataset_tlv))
else:
_LOGGER.debug(
"not importing TLV with channel %s", thread_dataset_channel
"not importing TLV with channel %s for %s",
thread_dataset_channel,
otbr_url,
)
pan_id = generate_random_pan_id()
await api.create_active_dataset(
......@@ -101,27 +109,65 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
)
await api.set_enabled(True)
async def _is_border_agent_id_configured(self, border_agent_id: bytes) -> bool:
"""Return True if another config entry's OTBR has the same border agent id."""
config_entry: OTBRConfigEntry
for config_entry in self.hass.config_entries.async_loaded_entries(DOMAIN):
data = config_entry.runtime_data
try:
other_border_agent_id = await data.get_border_agent_id()
except HomeAssistantError:
_LOGGER.debug(
"Could not read border agent id from %s", data.url, exc_info=True
)
continue
_LOGGER.debug(
"border agent id for existing url %s: %s",
data.url,
other_border_agent_id.hex(),
)
if border_agent_id == other_border_agent_id:
return True
return False
async def _connect_and_configure_router(self, otbr_url: str) -> bytes:
"""Connect to the router and configure it if needed.
Will raise if the router's border agent id is in use by another config entry.
Returns the router's border agent id.
"""
api = python_otbr_api.OTBR(otbr_url, async_get_clientsession(self.hass), 10)
border_agent_id = await api.get_border_agent_id()
_LOGGER.debug("border agent id for url %s: %s", otbr_url, border_agent_id.hex())
if await self._is_border_agent_id_configured(border_agent_id):
raise AlreadyConfigured
await self._set_dataset(api, otbr_url)
return border_agent_id
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Set up by user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
url = user_input[CONF_URL].rstrip("/")
try:
await self._connect_and_set_dataset(url)
border_agent_id = await self._connect_and_configure_router(url)
except AlreadyConfigured:
errors["base"] = "already_configured"
except (
python_otbr_api.OTBRError,
aiohttp.ClientError,
TimeoutError,
):
) as exc:
_LOGGER.debug("Failed to communicate with OTBR@%s: %s", url, exc)
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(DOMAIN)
await self.async_set_unique_id(border_agent_id.hex())
return self.async_create_entry(
title="Open Thread Border Router",
data={CONF_URL: url},
......@@ -140,34 +186,35 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN):
url = f"http://{config['host']}:{config['port']}"
config_entry_data = {"url": url}
if self._async_in_progress(include_uninitialized=True):
# We currently don't handle multiple config entries, abort if hassio
# discovers multiple addons with otbr support
return self.async_abort(reason="single_instance_allowed")
if current_entries := self._async_current_entries():
for current_entry in current_entries:
if current_entry.source != SOURCE_HASSIO:
continue
current_url = yarl.URL(current_entry.data["url"])
if (
if not (unique_id := current_entry.unique_id):
# The first version did not set a unique_id
# so if the entry does not have a unique_id
# we have to assume it's the first version
current_entry.unique_id
and (current_entry.unique_id != discovery_info.uuid)
# This check can be removed in HA Core 2025.9
unique_id = discovery_info.uuid
if (
unique_id != discovery_info.uuid
or current_url.host != config["host"]
or current_url.port == config["port"]
):
continue
# Update URL with the new port
self.hass.config_entries.async_update_entry(
current_entry, data=config_entry_data
current_entry,
data=config_entry_data,
unique_id=unique_id, # Remove in HA Core 2025.9
)
return self.async_abort(reason="single_instance_allowed")
return self.async_abort(reason="already_configured")
try:
await self._connect_and_set_dataset(url)
await self._connect_and_configure_router(url)
except AlreadyConfigured:
return self.async_abort(reason="already_configured")
except (
python_otbr_api.OTBRError,
aiohttp.ClientError,
......
......@@ -2,14 +2,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from .util import OTBRData
DOMAIN = "otbr"
DATA_OTBR: HassKey[OTBRData] = HassKey(DOMAIN)
DEFAULT_CHANNEL = 15
......@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from functools import wraps
import logging
from typing import Any, Concatenate
from typing import TYPE_CHECKING, Any, Concatenate
import aiohttp
from python_otbr_api import tlv_parser
......@@ -18,9 +18,12 @@ from homeassistant.components.thread import async_add_dataset
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_OTBR, DOMAIN
from .const import DOMAIN
from .util import OTBRData
if TYPE_CHECKING:
from . import OTBRConfigEntry
_LOGGER = logging.getLogger(__name__)
......@@ -45,15 +48,13 @@ def async_get_otbr_data[**_P, _R, _R_Def](
hass: HomeAssistant, *args: _P.args, **kwargs: _P.kwargs
) -> _R | _R_Def:
"""Fetch OTBR data and pass to orig_func."""
if DATA_OTBR not in hass.data:
return retval
data = hass.data[DATA_OTBR]
if not is_multiprotocol_url(data.url):
return retval
config_entry: OTBRConfigEntry
for config_entry in hass.config_entries.async_loaded_entries(DOMAIN):
data = config_entry.runtime_data
if is_multiprotocol_url(data.url):
return await orig_func(hass, data, *args, **kwargs)
return await orig_func(hass, data, *args, **kwargs)
return retval
return async_get_otbr_data_wrapper
......
......@@ -9,6 +9,7 @@
}
},
"error": {
"already_configured": "The Thread border router is already configured",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
......
......@@ -7,7 +7,7 @@ import dataclasses
from functools import wraps
import logging
import random
from typing import Any, Concatenate, cast
from typing import TYPE_CHECKING, Any, Concatenate, cast
import aiohttp
import python_otbr_api
......@@ -22,12 +22,16 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
multi_pan_addon_using_device,
)
from homeassistant.components.homeassistant_yellow import RADIO_DEVICE as YELLOW_RADIO
from homeassistant.config_entries import SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN
if TYPE_CHECKING:
from . import OTBRConfigEntry
_LOGGER = logging.getLogger(__name__)
INFO_URL_SKY_CONNECT = (
......@@ -48,6 +52,10 @@ INSECURE_PASSPHRASES = (
)
class GetBorderAgentIdNotSupported(HomeAssistantError):
"""Raised from python_otbr_api.GetBorderAgentIdNotSupportedError."""
def compose_default_network_name(pan_id: int) -> str:
"""Generate a default network name."""
return f"ha-thread-{pan_id:04x}"
......@@ -83,7 +91,7 @@ class OTBRData:
entry_id: str
@_handle_otbr_error
async def factory_reset(self) -> None:
async def factory_reset(self, hass: HomeAssistant) -> None:
"""Reset the router."""
try:
await self.api.factory_reset()
......@@ -92,14 +100,19 @@ class OTBRData:
"OTBR does not support factory reset, attempting to delete dataset"
)
await self.delete_active_dataset()
await update_unique_id(
hass,
hass.config_entries.async_get_entry(self.entry_id),
await self.get_border_agent_id(),
)
@_handle_otbr_error
async def get_border_agent_id(self) -> bytes | None:
async def get_border_agent_id(self) -> bytes:
"""Get the border agent ID or None if not supported by the router."""
try:
return await self.api.get_border_agent_id()
except python_otbr_api.GetBorderAgentIdNotSupportedError:
return None
except python_otbr_api.GetBorderAgentIdNotSupportedError as exc:
raise GetBorderAgentIdNotSupported from exc
@_handle_otbr_error
async def set_enabled(self, enabled: bool) -> None:
......@@ -258,3 +271,18 @@ async def update_issues(
"""Raise or clear repair issues related to network settings."""
await _warn_on_channel_collision(hass, otbrdata, dataset_tlvs)
_warn_on_default_network_settings(hass, otbrdata, dataset_tlvs)
async def update_unique_id(
hass: HomeAssistant, entry: OTBRConfigEntry | None, border_agent_id: bytes
) -> None:
"""Update the config entry's unique_id if not matching."""
border_agent_id_hex = border_agent_id.hex()
if entry and entry.source == SOURCE_USER and entry.unique_id != border_agent_id_hex:
_LOGGER.debug(
"Updating unique_id of entry %s from %s to %s",
entry.entry_id,
entry.unique_id,
border_agent_id_hex,
)
hass.config_entries.async_update_entry(entry, unique_id=border_agent_id_hex)
......@@ -2,7 +2,7 @@
from collections.abc import Callable, Coroutine
from functools import wraps
from typing import Any, cast
from typing import TYPE_CHECKING, Any, cast
import python_otbr_api
from python_otbr_api import PENDING_DATASET_DELAY_TIMER, tlv_parser
......@@ -17,7 +17,7 @@ from homeassistant.components.thread import async_add_dataset, async_get_dataset
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_OTBR, DEFAULT_CHANNEL, DOMAIN
from .const import DEFAULT_CHANNEL, DOMAIN
from .util import (
OTBRData,
compose_default_network_name,
......@@ -26,6 +26,9 @@ from .util import (
update_issues,
)
if TYPE_CHECKING:
from . import OTBRConfigEntry
@callback
def async_setup(hass: HomeAssistant) -> None:
......@@ -47,41 +50,45 @@ async def websocket_info(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Get OTBR info."""
if DATA_OTBR not in hass.data:
config_entries: list[OTBRConfigEntry]
config_entries = hass.config_entries.async_loaded_entries(DOMAIN)
if not config_entries:
connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
return
data = hass.data[DATA_OTBR]
response: dict[str, dict[str, Any]] = {}
try:
border_agent_id = await data.get_border_agent_id()
dataset = await data.get_active_dataset()
dataset_tlvs = await data.get_active_dataset_tlvs()
extended_address = (await data.get_extended_address()).hex()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "otbr_info_failed", str(exc))
return
for config_entry in config_entries:
data = config_entry.runtime_data
try:
border_agent_id = await data.get_border_agent_id()
dataset = await data.get_active_dataset()
dataset_tlvs = await data.get_active_dataset_tlvs()
extended_address = (await data.get_extended_address()).hex()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "otbr_info_failed", str(exc))
return
# The border agent ID is checked when the OTBR config entry is setup,
# we can assert it's not None
assert border_agent_id is not None
extended_pan_id = (
dataset.extended_pan_id.lower() if dataset and dataset.extended_pan_id else None
)
connection.send_result(
msg["id"],
{
extended_address: {
"active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None,
"border_agent_id": border_agent_id.hex(),
"channel": dataset.channel if dataset else None,
"extended_address": extended_address,
"extended_pan_id": extended_pan_id,
"url": data.url,
}
},
)
# The border agent ID is checked when the OTBR config entry is setup,
# we can assert it's not None
assert border_agent_id is not None
extended_pan_id = (
dataset.extended_pan_id.lower()
if dataset and dataset.extended_pan_id
else None
)
response[extended_address] = {
"active_dataset_tlvs": dataset_tlvs.hex() if dataset_tlvs else None,
"border_agent_id": border_agent_id.hex(),
"channel": dataset.channel if dataset else None,
"extended_address": extended_address,
"extended_pan_id": extended_pan_id,
"url": data.url,
}
connection.send_result(msg["id"], response)
def async_get_otbr_data(
......@@ -99,22 +106,29 @@ def async_get_otbr_data(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Fetch OTBR data and pass to orig_func."""
if DATA_OTBR not in hass.data:
config_entries: list[OTBRConfigEntry]
config_entries = hass.config_entries.async_loaded_entries(DOMAIN)
if not config_entries:
connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded")
return
data = hass.data[DATA_OTBR]
try:
extended_address = await data.get_extended_address()
except HomeAssistantError as exc:
connection.send_error(msg["id"], "get_extended_address_failed", str(exc))
return
if extended_address.hex() != msg["extended_address"]:
connection.send_error(msg["id"], "unknown_router", "")
for config_entry in config_entries:
data = config_entry.runtime_data
try:
extended_address = await data.get_extended_address()
except HomeAssistantError as exc:
connection.send_error(
msg["id"], "get_extended_address_failed", str(exc)
)
return
if extended_address.hex() != msg["extended_address"]:
continue
await orig_func(hass, connection, msg, data)
return
await orig_func(hass, connection, msg, data)
connection.send_error(msg["id"], "unknown_router", "")
return async_check_extended_address_func
......@@ -144,7 +158,7 @@ async def websocket_create_network(
return
try:
await data.factory_reset()
await data.factory_reset(hass)
except HomeAssistantError as exc:
connection.send_error(msg["id"], "factory_reset_failed", str(exc))
return
......
......@@ -31,6 +31,7 @@ DATASET_INSECURE_PASSPHRASE = bytes.fromhex(
TEST_BORDER_AGENT_EXTENDED_ADDRESS = bytes.fromhex("AEEB2F594B570BBF")
TEST_BORDER_AGENT_ID = bytes.fromhex("230C6A1AC57F6F4BE262ACF32E5EF52C")
TEST_BORDER_AGENT_ID_2 = bytes.fromhex("230C6A1AC57F6F4BE262ACF32E5EF52D")
ROUTER_DISCOVERY_HASS = {
"type_": "_meshcop._udp.local.",
......
......@@ -77,16 +77,18 @@ async def otbr_config_entry_multipan_fixture(
get_active_dataset_tlvs: AsyncMock,
get_border_agent_id: AsyncMock,
get_extended_address: AsyncMock,
) -> None:
) -> str:
"""Mock Open Thread Border Router config entry."""
config_entry = MockConfigEntry(
data=CONFIG_ENTRY_DATA_MULTIPAN,
domain=otbr.DOMAIN,
options={},
title="Open Thread Border Router",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
return config_entry.entry_id
@pytest.fixture(name="otbr_config_entry_thread")
......@@ -102,6 +104,7 @@ async def otbr_config_entry_thread_fixture(
domain=otbr.DOMAIN,
options={},
title="Open Thread Border Router",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
......
......@@ -13,7 +13,7 @@ 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 . 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
......@@ -57,12 +57,91 @@ def addon_info_fixture():
"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"
aioclient_mock.get(f"{stripped_url}/node/dataset/active", text="aa")
result = await hass.config_entries.flow.async_init(
otbr.DOMAIN, context={"source": "user"}
)
......@@ -88,13 +167,56 @@ async def test_user_flow(
assert result["options"] == {}
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0]
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 == otbr.DOMAIN
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:
......@@ -158,10 +280,11 @@ async def test_user_flow_router_not_setup(
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
assert config_entry.unique_id == TEST_BORDER_AGENT_ID.hex()
async def test_user_flow_404(
@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."""
......@@ -192,7 +315,30 @@ async def test_user_flow_404(
aiohttp.ClientError,
],
)
async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None:
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"}
......@@ -201,7 +347,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None:
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
with patch("python_otbr_api.OTBR.get_active_dataset_tlvs", side_effect=error):
with patch(f"python_otbr_api.OTBR.{func}", side_effect=error):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
......@@ -212,6 +358,7 @@ async def test_user_flow_connect_error(hass: HomeAssistant, error) -> None:
assert result["errors"] == {"base": "cannot_connect"}
@pytest.mark.usefixtures("get_border_agent_id")
async def test_hassio_discovery_flow(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info
) -> None:
......@@ -244,6 +391,7 @@ async def test_hassio_discovery_flow(
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, addon_info
) -> None:
......@@ -301,6 +449,7 @@ async def test_hassio_discovery_flow_yellow(
),
],
)
@pytest.mark.usefixtures("get_border_agent_id")
async def test_hassio_discovery_flow_sky_connect(
device: str,
title: str,
......@@ -346,6 +495,7 @@ async def test_hassio_discovery_flow_sky_connect(
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, addon_info
) -> None:
......@@ -354,6 +504,8 @@ async def test_hassio_discovery_flow_2x_addons(
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(hass: HomeAssistant, slug: str) -> dict[str, Any]:
await asyncio.sleep(0)
......@@ -387,18 +539,107 @@ async def test_hassio_discovery_flow_2x_addons(
addon_info.side_effect = _addon_info
with patch(
"homeassistant.components.otbr.async_setup_entry",
return_value=True,
) as mock_setup_entry:
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
)
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]
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, 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(hass: HomeAssistant, slug: str) -> dict[str, Any]:
await asyncio.sleep(0)
if slug == "otbr":
return {
"available": True,
"hostname": None,
"options": {
"device": (
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_"
"9e2adbd75b8beb119fe564a0f320645d-if00-port0"
)
},
"state": None,
"update_available": False,
"version": None,
}
return {
"available": True,
"hostname": None,
"options": {
"device": (
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_"
"9e2adbd75b8beb119fe564a0f320645d-if00-port1"
)
},
"state": None,
"update_available": False,
"version": None,
}
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']}",
......@@ -411,9 +652,8 @@ async def test_hassio_discovery_flow_2x_addons(
assert results[0]["data"] == expected_data
assert results[0]["options"] == {}
assert results[1]["type"] is FlowResultType.ABORT
assert results[1]["reason"] == "single_instance_allowed"
assert results[1]["reason"] == "already_configured"
assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0]
assert config_entry.data == expected_data
......@@ -424,6 +664,7 @@ async def test_hassio_discovery_flow_2x_addons(
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, addon_info
) -> None:
......@@ -481,6 +722,7 @@ async def test_hassio_discovery_flow_router_not_setup(
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, addon_info
) -> None:
......@@ -533,6 +775,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred(
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,
......@@ -596,6 +839,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2(
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:
......@@ -610,6 +854,7 @@ async def test_hassio_discovery_flow_404(
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:
......@@ -633,7 +878,7 @@ async def test_hassio_discovery_flow_new_port_missing_unique_id(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
assert result["reason"] == "already_configured"
expected_data = {
"url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}",
......@@ -642,6 +887,7 @@ async def test_hassio_discovery_flow_new_port_missing_unique_id(
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"))
......@@ -664,7 +910,7 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None:
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
assert result["reason"] == "already_configured"
expected_data = {
"url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}",
......@@ -673,6 +919,12 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None:
assert config_entry.data == expected_data
@pytest.mark.usefixtures(
"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"))
......@@ -691,22 +943,34 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) -
otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
# Another entry will be created
assert result["type"] is FlowResultType.CREATE_ENTRY
# Make sure the data was not updated
# 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_entries(otbr.DOMAIN)[0]
config_entry = hass.config_entries.async_get_entry(config_entry.entry_id)
assert config_entry.data == expected_data
@pytest.mark.parametrize(("source", "data"), [("hassio", HASSIO_DATA), ("user", None)])
async def test_config_flow_single_entry(
hass: HomeAssistant, source: str, data: Any
@pytest.mark.parametrize(
("source", "data", "expected_result"),
[
("hassio", HASSIO_DATA, FlowResultType.CREATE_ENTRY),
("user", None, FlowResultType.FORM),
],
)
@pytest.mark.usefixtures(
"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 only a single entry is allowed."""
"""Test more than a single entry is allowed."""
mock_integration(hass, MockModule("hassio"))
# Setup the config entry
......@@ -719,13 +983,11 @@ async def test_config_flow_single_entry(
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.homeassistant_yellow.async_setup_entry",
"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": source}, data=data
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
mock_setup_entry.assert_not_called()
assert result["type"] is expected_result
......@@ -11,6 +11,7 @@ from zeroconf.asyncio import AsyncServiceInfo
from homeassistant.components import otbr, thread
from homeassistant.components.thread import discovery
from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_USER
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
......@@ -18,7 +19,6 @@ from homeassistant.setup import async_setup_component
from . import (
BASE_URL,
CONFIG_ENTRY_DATA_MULTIPAN,
CONFIG_ENTRY_DATA_THREAD,
DATASET_CH15,
DATASET_CH16,
DATASET_INSECURE_NW_KEY,
......@@ -71,6 +71,7 @@ async def test_import_dataset(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
......@@ -138,6 +139,7 @@ async def test_import_share_radio_channel_collision(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
with (
......@@ -177,6 +179,7 @@ async def test_import_share_radio_no_channel_collision(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
with (
......@@ -214,6 +217,7 @@ async def test_import_insecure_dataset(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
with (
......@@ -252,6 +256,7 @@ async def test_config_entry_not_ready(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
get_active_dataset_tlvs.side_effect = error
......@@ -268,6 +273,7 @@ async def test_border_agent_id_not_supported(
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
get_border_agent_id.side_effect = python_otbr_api.GetBorderAgentIdNotSupportedError
......@@ -281,6 +287,7 @@ async def test_config_entry_update(hass: HomeAssistant) -> None:
domain=otbr.DOMAIN,
options={},
title="My OTBR",
unique_id=TEST_BORDER_AGENT_EXTENDED_ADDRESS.hex(),
)
config_entry.add_to_hass(hass)
mock_api = MagicMock()
......@@ -314,25 +321,33 @@ async def test_remove_entry(
await hass.config_entries.async_remove(config_entry.entry_id)
async def test_remove_extra_entries(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
@pytest.mark.parametrize(
("source", "unique_id", "updated_unique_id"),
[
(SOURCE_HASSIO, None, None),
(SOURCE_HASSIO, "abcd", "abcd"),
(SOURCE_USER, None, TEST_BORDER_AGENT_ID.hex()),
(SOURCE_USER, "abcd", TEST_BORDER_AGENT_ID.hex()),
],
)
async def test_update_unique_id(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
source: str,
unique_id: str | None,
updated_unique_id: str | None,
) -> None:
"""Test we remove additional config entries."""
"""Test we update the unique id if extended address has changed."""
config_entry1 = MockConfigEntry(
config_entry = MockConfigEntry(
data=CONFIG_ENTRY_DATA_MULTIPAN,
domain=otbr.DOMAIN,
options={},
source=source,
title="Open Thread Border Router",
unique_id=unique_id,
)
config_entry2 = MockConfigEntry(
data=CONFIG_ENTRY_DATA_THREAD,
domain=otbr.DOMAIN,
options={},
title="Open Thread Border Router",
)
config_entry1.add_to_hass(hass)
config_entry2.add_to_hass(hass)
assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 2
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, otbr.DOMAIN, {})
assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1
config_entry = hass.config_entries.async_get_entry(config_entry.entry_id)
assert config_entry.unique_id == updated_unique_id
......@@ -5,7 +5,6 @@ from unittest.mock import patch
import pytest
from python_otbr_api import ActiveDataSet, tlv_parser
from homeassistant.components import otbr
from homeassistant.components.otbr import (
silabs_multiprotocol as otbr_silabs_multiprotocol,
)
......@@ -127,10 +126,11 @@ async def test_async_change_channel_no_otbr(hass: HomeAssistant) -> None:
async def test_async_change_channel_non_matching_url(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test async_change_channel when otbr is not configured."""
hass.data[otbr.DATA_OTBR].url = OTBR_NON_MULTIPAN_URL
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
config_entry.runtime_data.url = OTBR_NON_MULTIPAN_URL
with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel:
await otbr_silabs_multiprotocol.async_change_channel(hass, 16, delay=0)
mock_set_channel.assert_not_awaited()
......@@ -184,10 +184,11 @@ async def test_async_get_channel_no_otbr(hass: HomeAssistant) -> None:
async def test_async_get_channel_non_matching_url(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test async_change_channel when otbr is not configured."""
hass.data[otbr.DATA_OTBR].url = OTBR_NON_MULTIPAN_URL
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
config_entry.runtime_data.url = OTBR_NON_MULTIPAN_URL
with patch("python_otbr_api.OTBR.get_active_dataset") as mock_get_active_dataset:
assert await otbr_silabs_multiprotocol.async_get_channel(hass) is None
mock_get_active_dataset.assert_not_awaited()
......@@ -198,10 +199,11 @@ async def test_async_get_channel_non_matching_url(
[(OTBR_MULTIPAN_URL, True), (OTBR_NON_MULTIPAN_URL, False)],
)
async def test_async_using_multipan(
hass: HomeAssistant, otbr_config_entry_multipan, url: str, expected: bool
hass: HomeAssistant, otbr_config_entry_multipan: str, url: str, expected: bool
) -> None:
"""Test async_change_channel when otbr is not configured."""
hass.data[otbr.DATA_OTBR].url = url
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
config_entry.runtime_data.url = url
assert await otbr_silabs_multiprotocol.async_using_multipan(hass) is expected
......@@ -213,8 +215,9 @@ async def test_async_using_multipan_no_otbr(hass: HomeAssistant) -> None:
async def test_async_using_multipan_non_matching_url(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test async_change_channel when otbr is not configured."""
hass.data[otbr.DATA_OTBR].url = OTBR_NON_MULTIPAN_URL
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
config_entry.runtime_data.url = OTBR_NON_MULTIPAN_URL
assert await otbr_silabs_multiprotocol.async_using_multipan(hass) is False
"""Test OTBR Utility functions."""
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
import pytest
import python_otbr_api
......@@ -31,24 +31,37 @@ async def test_get_allowed_channel(
assert await otbr.util.get_allowed_channel(hass, OTBR_NON_MULTIPAN_URL) is None
async def test_factory_reset(hass: HomeAssistant, otbr_config_entry_multipan) -> None:
async def test_factory_reset(
hass: HomeAssistant,
otbr_config_entry_multipan: str,
get_border_agent_id: AsyncMock,
) -> None:
"""Test factory_reset."""
new_ba_id = b"new_ba_id"
get_border_agent_id.return_value = new_ba_id
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
assert config_entry.unique_id != new_ba_id.hex()
with (
patch("python_otbr_api.OTBR.factory_reset") as factory_reset_mock,
patch(
"python_otbr_api.OTBR.delete_active_dataset"
) as delete_active_dataset_mock,
):
await hass.data[otbr.DATA_OTBR].factory_reset()
await config_entry.runtime_data.factory_reset(hass)
delete_active_dataset_mock.assert_not_called()
factory_reset_mock.assert_called_once_with()
# Check the unique_id is updated
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
assert config_entry.unique_id == new_ba_id.hex()
async def test_factory_reset_not_supported(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test factory_reset."""
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
with (
patch(
"python_otbr_api.OTBR.factory_reset",
......@@ -58,16 +71,17 @@ async def test_factory_reset_not_supported(
"python_otbr_api.OTBR.delete_active_dataset"
) as delete_active_dataset_mock,
):
await hass.data[otbr.DATA_OTBR].factory_reset()
await config_entry.runtime_data.factory_reset(hass)
delete_active_dataset_mock.assert_called_once_with()
factory_reset_mock.assert_called_once_with()
async def test_factory_reset_error_1(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test factory_reset."""
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
with (
patch(
"python_otbr_api.OTBR.factory_reset",
......@@ -80,16 +94,17 @@ async def test_factory_reset_error_1(
HomeAssistantError,
),
):
await hass.data[otbr.DATA_OTBR].factory_reset()
await config_entry.runtime_data.factory_reset(hass)
delete_active_dataset_mock.assert_not_called()
factory_reset_mock.assert_called_once_with()
async def test_factory_reset_error_2(
hass: HomeAssistant, otbr_config_entry_multipan
hass: HomeAssistant, otbr_config_entry_multipan: str
) -> None:
"""Test factory_reset."""
config_entry = hass.config_entries.async_get_entry(otbr_config_entry_multipan)
with (
patch(
"python_otbr_api.OTBR.factory_reset",
......@@ -103,7 +118,7 @@ async def test_factory_reset_error_2(
HomeAssistantError,
),
):
await hass.data[otbr.DATA_OTBR].factory_reset()
await config_entry.runtime_data.factory_reset(hass)
delete_active_dataset_mock.assert_called_once_with()
factory_reset_mock.assert_called_once_with()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment