diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index acade3afba..808fec6b53 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
@@ -33,7 +33,7 @@ import org.thingsboard.rule.engine.metadata.TbFetchDeviceCredentialsNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetCustomerAttributeNode;
import org.thingsboard.rule.engine.metadata.TbGetCustomerDetailsNode;
-import org.thingsboard.rule.engine.metadata.TbGetDeviceAttrNode;
+import org.thingsboard.rule.engine.metadata.TbGetRelatedDeviceAttrNode;
import org.thingsboard.rule.engine.metadata.TbGetOriginatorFieldsNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode;
import org.thingsboard.rule.engine.metadata.TbGetTenantAttributeNode;
@@ -232,7 +232,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
TbGetOriginatorFieldsNode.class.getName(),
TbFetchDeviceCredentialsNode.class.getName(),
TbGetAttributesNode.class.getName(),
- TbGetDeviceAttrNode.class.getName(),
+ TbGetRelatedDeviceAttrNode.class.getName(),
TbGetRelatedAttributeNode.class.getName(),
TbGetTenantAttributeNode.class.getName(),
TbGetCustomerAttributeNode.class.getName(),
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDeviceAttrNode.java
similarity index 96%
rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDeviceAttrNode.java
index fdbb412e6b..53220837a9 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDeviceAttrNode.java
@@ -39,7 +39,7 @@ import org.thingsboard.server.common.msg.TbMsg;
"metadata.cs_temperature or metadata.shared_limit ",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeDeviceAttributesConfig")
-public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode {
+public class TbGetRelatedDeviceAttrNode extends TbAbstractGetAttributesNode {
private static final String RELATED_DEVICE_NOT_FOUND_MESSAGE = "Failed to find related device to message originator using relation query specified in the configuration!";
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java
new file mode 100644
index 0000000000..dfea960c07
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java
@@ -0,0 +1,117 @@
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.Futures;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class EntitiesRelatedDeviceIdAsyncLoaderTest {
+
+ private static final EntityId DUMMY_ORIGINATOR = new DeviceId(UUID.randomUUID());
+ private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
+ @Mock
+ private TbContext ctxMock;
+ @Mock
+ private DeviceService deviceServiceMock;
+
+ @Test
+ public void givenDeviceRelationsQuery_whenFindDeviceAsync_ShouldBuildCorrectDeviceSearchQuery() {
+ // GIVEN
+ var deviceRelationsQuery = new DeviceRelationsQuery();
+ deviceRelationsQuery.setDeviceTypes(List.of("Device type 1", "Device type 2", "default"));
+ deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);
+ deviceRelationsQuery.setMaxLevel(2);
+ deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE);
+
+ var expectedDeviceSearchQuery = new DeviceSearchQuery();
+ var parameters = new RelationsSearchParameters(
+ DUMMY_ORIGINATOR,
+ deviceRelationsQuery.getDirection(),
+ deviceRelationsQuery.getMaxLevel(),
+ deviceRelationsQuery.isFetchLastLevelOnly()
+ );
+ expectedDeviceSearchQuery.setParameters(parameters);
+ expectedDeviceSearchQuery.setRelationType(deviceRelationsQuery.getRelationType());
+ expectedDeviceSearchQuery.setDeviceTypes(deviceRelationsQuery.getDeviceTypes());
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
+ when(deviceServiceMock.findDevicesByQuery(eq(TENANT_ID), eq(expectedDeviceSearchQuery)))
+ .thenReturn(Futures.immediateFuture(null));
+
+ // WHEN
+ EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctxMock, DUMMY_ORIGINATOR, deviceRelationsQuery);
+
+ // THEN
+ verify(deviceServiceMock, times(1)).findDevicesByQuery(eq(TENANT_ID), eq(expectedDeviceSearchQuery));
+ }
+
+ @Test
+ public void givenSeveralDevicesFound_whenFindDeviceAsync_ShouldKeepOneAndDiscardOthers() throws Exception {
+ // GIVEN
+ var deviceRelationsQuery = new DeviceRelationsQuery();
+ deviceRelationsQuery.setDeviceTypes(List.of("Device type 1", "Device type 2", "default"));
+ deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);
+ deviceRelationsQuery.setMaxLevel(2);
+ deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE);
+
+ var expectedDeviceSearchQuery = new DeviceSearchQuery();
+ var parameters = new RelationsSearchParameters(
+ DUMMY_ORIGINATOR,
+ deviceRelationsQuery.getDirection(),
+ deviceRelationsQuery.getMaxLevel(),
+ deviceRelationsQuery.isFetchLastLevelOnly()
+ );
+ expectedDeviceSearchQuery.setParameters(parameters);
+ expectedDeviceSearchQuery.setRelationType(deviceRelationsQuery.getRelationType());
+ expectedDeviceSearchQuery.setDeviceTypes(deviceRelationsQuery.getDeviceTypes());
+
+ var device1 = new Device(new DeviceId(UUID.randomUUID()));
+ device1.setName("Device 1");
+ var device2 = new Device(new DeviceId(UUID.randomUUID()));
+ device1.setName("Device 2");
+ var device3 = new Device(new DeviceId(UUID.randomUUID()));
+ device1.setName("Device 3");
+
+ var devicesList = List.of(device1, device2, device3);
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
+ when(deviceServiceMock.findDevicesByQuery(eq(TENANT_ID), eq(expectedDeviceSearchQuery)))
+ .thenReturn(Futures.immediateFuture(devicesList));
+
+ // WHEN
+ var entityIdFuture = EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctxMock, DUMMY_ORIGINATOR, deviceRelationsQuery);
+
+ // THEN
+ assertNotNull(entityIdFuture);
+
+ var actualEntityId = entityIdFuture.get();
+ assertNotNull(actualEntityId);
+ assertEquals(device1.getId(), actualEntityId);
+ }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntitiesIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntitiesIdAsyncLoaderTest.java
new file mode 100644
index 0000000000..2b27ce12bf
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntitiesIdAsyncLoaderTest.java
@@ -0,0 +1,138 @@
+package org.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.Futures;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.RelationService;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class EntitiesRelatedEntitiesIdAsyncLoaderTest {
+
+ private static final EntityId DUMMY_ORIGINATOR = new DeviceId(UUID.randomUUID());
+ private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
+ @Mock
+ private TbContext ctxMock;
+ @Mock
+ private RelationService relationServiceMock;
+
+ @Test
+ public void givenRelationsQuery_whenFindEntityAsync_ShouldBuildCorrectEntityRelationsQuery() {
+ // GIVEN
+ var relationsQuery = new RelationsQuery();
+ var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(1);
+ relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
+
+ var expectedEntityRelationsQuery = new EntityRelationsQuery();
+ var parameters = new RelationsSearchParameters(
+ DUMMY_ORIGINATOR,
+ relationsQuery.getDirection(),
+ relationsQuery.getMaxLevel(),
+ relationsQuery.isFetchLastLevelOnly()
+ );
+ expectedEntityRelationsQuery.setParameters(parameters);
+ expectedEntityRelationsQuery.setFilters(relationsQuery.getFilters());
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
+ when(relationServiceMock.findByQuery(eq(TENANT_ID), eq(expectedEntityRelationsQuery)))
+ .thenReturn(Futures.immediateFuture(null));
+
+ // WHEN
+ EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctxMock, DUMMY_ORIGINATOR, relationsQuery);
+
+ // THEN
+ verify(relationServiceMock, times(1)).findByQuery(eq(TENANT_ID), eq(expectedEntityRelationsQuery));
+ }
+
+ @Test
+ public void givenSeveralEntitiesFound_whenFindEntityAsync_ShouldKeepOneAndDiscardOthers() throws Exception {
+ // GIVEN
+ var relationsQuery = new RelationsQuery();
+ var relationEntityTypeFilter = new RelationEntityTypeFilter(
+ EntityRelation.CONTAINS_TYPE,
+ List.of(EntityType.DEVICE, EntityType.ASSET)
+ );
+ relationsQuery.setDirection(EntitySearchDirection.FROM);
+ relationsQuery.setMaxLevel(2);
+ relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
+
+ var expectedEntityRelationsQuery = new EntityRelationsQuery();
+ var parameters = new RelationsSearchParameters(
+ DUMMY_ORIGINATOR,
+ relationsQuery.getDirection(),
+ relationsQuery.getMaxLevel(),
+ relationsQuery.isFetchLastLevelOnly()
+ );
+ expectedEntityRelationsQuery.setParameters(parameters);
+ expectedEntityRelationsQuery.setFilters(relationsQuery.getFilters());
+
+ var device1 = new Device(new DeviceId(UUID.randomUUID()));
+ device1.setName("Device 1");
+ var device2 = new Device(new DeviceId(UUID.randomUUID()));
+ device1.setName("Device 2");
+ var asset = new Asset(new AssetId(UUID.randomUUID()));
+ asset.setName("Asset");
+
+ var entityRelationDevice1 = new EntityRelation();
+ entityRelationDevice1.setFrom(DUMMY_ORIGINATOR);
+ entityRelationDevice1.setTo(device1.getId());
+ entityRelationDevice1.setType(EntityRelation.CONTAINS_TYPE);
+
+ var entityRelationDevice2 = new EntityRelation();
+ entityRelationDevice2.setFrom(DUMMY_ORIGINATOR);
+ entityRelationDevice2.setTo(device2.getId());
+ entityRelationDevice2.setType(EntityRelation.CONTAINS_TYPE);
+
+ var entityRelationAsset = new EntityRelation();
+ entityRelationAsset.setFrom(DUMMY_ORIGINATOR);
+ entityRelationAsset.setTo(asset.getId());
+ entityRelationAsset.setType(EntityRelation.CONTAINS_TYPE);
+
+ var expectedEntityRelationsList = List.of(entityRelationDevice1, entityRelationDevice2, entityRelationAsset);
+
+ when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
+ when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
+ when(relationServiceMock.findByQuery(eq(TENANT_ID), eq(expectedEntityRelationsQuery)))
+ .thenReturn(Futures.immediateFuture(expectedEntityRelationsList));
+
+ // WHEN
+ var deviceIdFuture = EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctxMock, DUMMY_ORIGINATOR, relationsQuery);
+
+ // THEN
+ assertNotNull(deviceIdFuture);
+
+ var actualDeviceId = deviceIdFuture.get();
+ assertNotNull(actualDeviceId);
+ assertEquals(device1.getId(), actualDeviceId);
+ }
+
+}