diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py
index 3e53358a16255d1d53373a2957c732d33b8e5469..4b95be1d40d67816e1a16ab0fec18a773aa75a81 100644
--- a/homeassistant/components/otbr/__init__.py
+++ b/homeassistant/components/otbr/__init__.py
@@ -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)
diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py
index 8cffc0a99e6df432d1ea97bf3e5d79c2a1342f0a..c1747981b07debea5ff434931955d54c4abad31d 100644
--- a/homeassistant/components/otbr/config_flow.py
+++ b/homeassistant/components/otbr/config_flow.py
@@ -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,
diff --git a/homeassistant/components/otbr/const.py b/homeassistant/components/otbr/const.py
index cf1678466a44afca9ae193bb0b5fdde9cc7006a2..c38b3cc125097c2ce5ad4c1de4a0f81b77ff2ac8 100644
--- a/homeassistant/components/otbr/const.py
+++ b/homeassistant/components/otbr/const.py
@@ -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
diff --git a/homeassistant/components/otbr/silabs_multiprotocol.py b/homeassistant/components/otbr/silabs_multiprotocol.py
index b3a711968fda9d0035f576e001ee10429002c57d..d97e6811e6d19e7b8e311d8100495b0bb93bc442 100644
--- a/homeassistant/components/otbr/silabs_multiprotocol.py
+++ b/homeassistant/components/otbr/silabs_multiprotocol.py
@@ -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
 
diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json
index 838ebeb5b8cb732f22c4f4018a82329de514db57..bc7812c1db777abe881844771bb3654220bb81ba 100644
--- a/homeassistant/components/otbr/strings.json
+++ b/homeassistant/components/otbr/strings.json
@@ -9,6 +9,7 @@
       }
     },
     "error": {
+      "already_configured": "The Thread border router is already configured",
       "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
     },
     "abort": {
diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py
index d426ca9ba17aa7e741fcae41b34e6d1598af2c49..351e23c7736a089a80718734d19de92d727d65f7 100644
--- a/homeassistant/components/otbr/util.py
+++ b/homeassistant/components/otbr/util.py
@@ -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)
diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py
index 577f9cc381d8112069d1294bee32f519856516ce..2bcd0da8f16c50e59624ddbce4e945effd72b461 100644
--- a/homeassistant/components/otbr/websocket_api.py
+++ b/homeassistant/components/otbr/websocket_api.py
@@ -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
diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py
index 2c9daa127c2832a837723d200731847c106650d6..7d52318b477f830e17f90196a4bb5af925e3d9d3 100644
--- a/tests/components/otbr/__init__.py
+++ b/tests/components/otbr/__init__.py
@@ -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.",
diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py
index 3811ff66ebb2249eb0a229f81ea1b8653cb1f002..5ab3e4421830ac2128e76ffe4672325790dfec33 100644
--- a/tests/components/otbr/conftest.py
+++ b/tests/components/otbr/conftest.py
@@ -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)
diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py
index c4972bb5f836cafb3bd68ce46e32da116daa732d..edd92591b1bce9355e247ec45e501bb53c3e0650 100644
--- a/tests/components/otbr/test_config_flow.py
+++ b/tests/components/otbr/test_config_flow.py
@@ -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
diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py
index 86bab71cbdab7931006482ca7270405386e45098..ca1cbd6483b616e6fcf697f1aedbd4a4f526d9d0 100644
--- a/tests/components/otbr/test_init.py
+++ b/tests/components/otbr/test_init.py
@@ -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
diff --git a/tests/components/otbr/test_silabs_multiprotocol.py b/tests/components/otbr/test_silabs_multiprotocol.py
index e842f40ad4c5966302b98da62411217da32bcebc..01b1ab63f56cb5ea67a0c16a93688763c47834d1 100644
--- a/tests/components/otbr/test_silabs_multiprotocol.py
+++ b/tests/components/otbr/test_silabs_multiprotocol.py
@@ -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
diff --git a/tests/components/otbr/test_util.py b/tests/components/otbr/test_util.py
index ec325b8819e896d3de5ce3f39816bb34554e6eb2..0ed3041bea84ce2b2ba0b878b817631ccc025486 100644
--- a/tests/components/otbr/test_util.py
+++ b/tests/components/otbr/test_util.py
@@ -1,6 +1,6 @@
 """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()