Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/entity-alarm-rules

pull/14193/head
VIacheslavKlimov 8 months ago
parent
commit
2155752f6b
  1. 48
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  3. 5
      application/src/main/resources/thingsboard.yml
  4. 53
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  5. 29
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  6. 15
      dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java
  7. 12
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java
  8. 63
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java
  9. 12
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java
  10. 41
      dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java
  11. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java
  12. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  13. 35
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java
  14. 21
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  15. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java
  16. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java
  17. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java
  18. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  19. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java
  20. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java
  21. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java
  22. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
  23. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java
  24. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  25. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  26. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  27. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
  28. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java
  29. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java
  30. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java
  31. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java
  32. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java
  33. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java
  34. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java
  35. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java
  36. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  37. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  38. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
  39. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java
  40. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java
  41. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
  42. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java
  43. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java
  44. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java
  45. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java
  46. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java
  47. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java
  48. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java
  49. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  50. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
  51. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  52. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  53. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java
  54. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java
  55. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  56. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  57. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  58. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  59. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java
  60. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  61. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
  62. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
  63. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java
  64. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  65. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java
  66. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java
  67. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java
  68. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  69. 35
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  70. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  71. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java
  72. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java
  73. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  74. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  75. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java
  76. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java
  77. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java
  78. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
  79. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  80. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
  81. 12
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
  82. 21
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
  83. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
  84. 13
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
  85. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  86. 9
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java
  87. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  88. 19
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  89. 149
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html
  90. 33
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss
  91. 79
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts
  92. 20
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
  93. 4
      ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts
  94. 2
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  95. 1
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  96. 14
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  97. 145
      ui-ngx/src/app/shared/models/constants.ts
  98. 7
      ui-ngx/src/app/shared/models/rule-node.models.ts
  99. 27
      ui-ngx/src/assets/locale/locale.constant-en_US.json

48
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -306,27 +306,28 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
newCfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) {
if (oldCtx.getCfId().equals(newCf.getId())) {
} finally {
calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) {
if (oldCtx.getCfId().equals(newCf.getId())) {
newCfList.add(newCfCtx);
found = true;
} else {
newCfList.add(oldCtx);
}
}
if (!found) {
newCfList.add(newCfCtx);
found = true;
} else {
newCfList.add(oldCtx);
}
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
}
if (!found) {
newCfList.add(newCfCtx);
}
entityIdCalculatedFields.put(newCf.getEntityId(), newCfList);
deleteLinks(oldCfCtx);
addLinks(newCf);
StateAction stateAction;
if (newCfCtx.getCfType() != oldCfCtx.getCfType()) {
@ -340,8 +341,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return;
}
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb));
}
}
@ -588,11 +587,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
} finally {
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
}
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -180,6 +180,7 @@ public class CalculatedFieldCtx {
initTbelExpression(expression);
initialized = true;
} catch (Exception e) {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
}
}
@ -273,6 +274,7 @@ public class CalculatedFieldCtx {
);
simpleExpressions.put(expression, compiledExpression);
} else {
initialized = false;
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
}
}

5
application/src/main/resources/thingsboard.yml

@ -306,8 +306,11 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# Enable/Disable timestamp key-value partioning on read queries
# Enable/Disable timestamp key-value partitioning on read queries
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
# Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value.
# It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database
use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}"
# The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
# Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired

53
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -618,6 +618,59 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("M + 1");
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("m", argument));
config.setExpression("m + 1");
Output output = new Output();
output.setName("m1");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(0);
config.setOutput(output);
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}"));
await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
config.setExpression("m m");
calculatedField.setConfiguration(config);
calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}"));
await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1");
assertThat(m1).isNotNull();
assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2");
});
}
@Test
public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception {
// --- Arrange entities ---

29
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -113,6 +113,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
@Value("${cassandra.query.use_ts_key_value_partitioning_on_read:true}")
private boolean useTsKeyValuePartitioningOnRead;
@Getter
@Value("${cassandra.query.use_ts_key_value_partitioning_on_read_max_estimated_partition_count:40}") // 3+ years for MONTHS
private int useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount;
@Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}")
private long partitionsCacheSize;
@ -415,22 +419,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
readResultsProcessingExecutor);
}
private ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
ListenableFuture<List<Long>> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
if (isFixedPartitioning()) { //no need to fetch partitions from DB
return Futures.immediateFuture(FIXED_PARTITION);
}
if (!isUseTsKeyValuePartitioningOnRead()) {
return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition));
final long estimatedPartitionCount = estimatePartitionCount(minPartition, maxPartition);
if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) {
return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition, (int) estimatedPartitionCount));
}
}
return getPartitionsFromDB(tenantId, query, entityId, minPartition, maxPartition);
}
ListenableFuture<List<Long>> getPartitionsFromDB(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) {
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
}
// Optimistic estimation of partition count, expected to be never called for infinite partitioning
long estimatePartitionCount(long minPartition, long maxPartition) {
if (maxPartition > minPartition) {
return (maxPartition - minPartition) / tsFormat.getDurationMs() + 2; //at least 2 partitions, at max 2 partitions overestimated
}
return 1; // 1 or 0, but 1 is more optimistic
}
List<Long> calculatePartitions(long minPartition, long maxPartition) {
return calculatePartitions(minPartition, maxPartition, 0);
}
List<Long> calculatePartitions(long minPartition, long maxPartition, int estimatedPartitionCount) {
if (minPartition == maxPartition) {
return Collections.singletonList(minPartition);
}
List<Long> partitions = new ArrayList<>();
List<Long> partitions = estimatedPartitionCount > 0 ? new ArrayList<>(estimatedPartitionCount) : new ArrayList<>();
long currentPartition = minPartition;
LocalDateTime currentPartitionTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentPartition), ZoneOffset.UTC);

15
dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java

@ -15,32 +15,29 @@
*/
package org.thingsboard.server.dao.timeseries;
import lombok.Getter;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Getter
public enum NoSqlTsPartitionDate {
MINUTES("yyyy-MM-dd-HH-mm", ChronoUnit.MINUTES), HOURS("yyyy-MM-dd-HH", ChronoUnit.HOURS), DAYS("yyyy-MM-dd", ChronoUnit.DAYS), MONTHS("yyyy-MM", ChronoUnit.MONTHS), YEARS("yyyy", ChronoUnit.YEARS),INDEFINITE("",ChronoUnit.FOREVER);
private final String pattern;
private final transient TemporalUnit truncateUnit;
private final transient long durationMs;
public final static LocalDateTime EPOCH_START = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC);
NoSqlTsPartitionDate(String pattern, TemporalUnit truncateUnit) {
this.pattern = pattern;
this.truncateUnit = truncateUnit;
}
public String getPattern() {
return pattern;
}
public TemporalUnit getTruncateUnit() {
return truncateUnit;
this.durationMs = TimeUnit.SECONDS.toMillis(this.truncateUnit.getDuration().getSeconds());
}
public LocalDateTime truncatedTo(LocalDateTime time) {

12
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java

@ -117,4 +117,16 @@ public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest {
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime()));
}
@Test
public void testEstimatePartitionCount() throws ParseException {
assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(153_722_867_280_914L);
assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L);
long startTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("600,479 minutes + 2 spare periods").isEqualTo(600479 + 2);
assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L);
}
}

63
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java

@ -15,25 +15,35 @@
*/
package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import java.util.UUID;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@ -50,7 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest {
@Autowired
@MockitoSpyBean
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@ -131,4 +141,53 @@ public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest {
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
}
@Test
public void testEstimatePartitionCount() throws ParseException {
assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(3_507_324_297L);
assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L);
long startTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("13 month + 2 spare periods").isEqualTo(13 + 2);
assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L);
}
@Test
public void testGetPartitionsFutureModeratePartitionsCount() throws ParseException {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TsKvQuery query = mock(TsKvQuery.class);
long startTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs);
tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs);
verify(tsDao).estimatePartitionCount(startTs, endTs);
verify(tsDao).calculatePartitions(eq(startTs), eq(endTs), anyInt());
verify(tsDao, never()).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs);
}
@Test
public void testGetPartitionsFutureHugePartitionsCountPreventOOMFallbackToDB() throws ParseException {
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TsKvQuery query = mock(TsKvQuery.class);
long startTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2000-12-12T00:00:00Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("3000-01-31T23:59:59Z").getTime());
willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs);
tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs);
verify(tsDao).estimatePartitionCount(startTs, endTs);
verify(tsDao, never()).calculatePartitions(eq(startTs), eq(endTs), anyInt());
verify(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs);
}
}

12
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java

@ -112,4 +112,16 @@ public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest {
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime()));
}
@Test
public void testEstimatePartitionCount() throws ParseException {
assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(292_277_026L);
assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L);
long startTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("2 years + 2 spare periods").isEqualTo(2 + 2);
assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L);
}
}

41
dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.timeseries;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.assertj.core.api.Assertions.assertThat;
class NoSqlTsPartitionDateTest {
@ParameterizedTest
@EnumSource(NoSqlTsPartitionDate.class)
void getDurationMsTest(NoSqlTsPartitionDate tsPartitionDate) throws Exception {
final Long durationMs = switch (tsPartitionDate) {
case MINUTES -> 60000L;
case HOURS -> 3600000L;
case DAYS -> 86400000L;
case MONTHS -> 2629746000L;
case YEARS -> 31556952000L;
case INDEFINITE -> Long.MAX_VALUE;
default -> null; //should be here in case a new enum value will be added in future
};
assertThat(durationMs).isNotNull();
assertThat(tsPartitionDate.getDurationMs()).isEqualTo(durationMs);
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "assign to customer",
@ -42,7 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg;
"Rule node will create a new customer if it doesn't exist, and 'Create new customer if it doesn't exist' enabled.",
configDirective = "tbActionNodeAssignToCustomerConfig",
icon = "add_circle",
version = 1
version = 1,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/"
)
public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssignToCustomerNodeConfiguration> {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java

@ -45,7 +45,8 @@ import static com.google.common.util.concurrent.Futures.transformAsync;
Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>.
Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.""",
configDirective = "tbActionNodeClearAlarmConfig",
icon = "notifications_off"
icon = "notifications_off",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/"
)
public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfiguration> {

35
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java

@ -21,7 +21,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
@ -30,8 +30,6 @@ import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
@ -53,7 +51,6 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "copy to view",
@ -63,16 +60,13 @@ import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCES
"Copy will be done only for attributes that are between start and end dates and according to attribute keys configuration. \n" +
"Changes message originator to related entity view and produces new messages according to count of updated entity views",
configDirective = "tbNodeEmptyConfig",
icon = "content_copy"
icon = "content_copy",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/copy-to-view/"
)
public class TbCopyAttributesToEntityViewNode implements TbNode {
EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
@ -138,14 +132,14 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
}
private FutureCallback<Void> getFutureCallback(TbContext ctx, TbMsg msg, EntityView entityView) {
return new FutureCallback<Void>() {
return new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void result) {
transformAndTellNext(ctx, msg, entityView);
}
@Override
public void onFailure(Throwable t) {
public void onFailure(@NonNull Throwable t) {
ctx.tellFailure(msg, t);
}
};
@ -157,18 +151,11 @@ public class TbCopyAttributesToEntityViewNode implements TbNode {
private boolean attributeContainsInEntityView(AttributeScope scope, String attrKey, EntityView entityView) {
AttributesEntityView attributesEntityView = entityView.getKeys().getAttributes();
List<String> keys = null;
switch (scope) {
case CLIENT_SCOPE:
keys = attributesEntityView.getCs();
break;
case SERVER_SCOPE:
keys = attributesEntityView.getSs();
break;
case SHARED_SCOPE:
keys = attributesEntityView.getSh();
break;
}
List<String> keys = switch (scope) {
case CLIENT_SCOPE -> attributesEntityView.getCs();
case SERVER_SCOPE -> attributesEntityView.getSs();
case SHARED_SCOPE -> attributesEntityView.getSh();
};
return CollectionsUtil.contains(keys, attrKey);
}

21
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.EnumUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
@ -39,7 +38,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.io.IOException;
import java.util.List;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "create alarm", relationTypes = {"Created", "Updated", "False"},
@ -52,7 +50,8 @@ import java.util.List;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
configDirective = "tbActionNodeCreateAlarmConfig",
icon = "notifications_active"
icon = "notifications_active",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/"
)
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
@ -62,10 +61,10 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx, configuration);
if (!this.config.isDynamicSeverity()) {
this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity());
if (this.notDynamicAlarmSeverity == null) {
throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity(), true);
if (!config.isDynamicSeverity()) {
notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, config.getSeverity());
if (notDynamicAlarmSeverity == null) {
throw new TbNodeException("Incorrect Alarm Severity value: " + config.getSeverity(), true);
}
}
}
@ -83,7 +82,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
final Alarm msgAlarm;
if (!config.isUseMessageAlarmData()) {
alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg);
alarmType = TbNodeUtils.processPattern(config.getAlarmType(), msg);
msgAlarm = null;
} else {
try {
@ -180,11 +179,11 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
.originator(msg.getOriginator())
.cleared(false)
.acknowledged(false)
.severity(this.config.isDynamicSeverity() ? processAlarmSeverity(msg) : notDynamicAlarmSeverity)
.severity(config.isDynamicSeverity() ? processAlarmSeverity(msg) : notDynamicAlarmSeverity)
.propagate(config.isPropagate())
.propagateToOwner(config.isPropagateToOwner())
.propagateToTenant(config.isPropagateToTenant())
.type(TbNodeUtils.processPattern(this.config.getAlarmType(), msg))
.type(TbNodeUtils.processPattern(config.getAlarmType(), msg))
.propagateRelationTypes(relationTypes)
.startTs(ts)
.endTs(ts)
@ -193,7 +192,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
}
private AlarmSeverity processAlarmSeverity(TbMsg msg) {
AlarmSeverity severity = EnumUtils.getEnum(AlarmSeverity.class, TbNodeUtils.processPattern(this.config.getSeverity(), msg));
AlarmSeverity severity = EnumUtils.getEnum(AlarmSeverity.class, TbNodeUtils.processPattern(config.getSeverity(), msg));
if (severity == null) {
throw new RuntimeException("Used incorrect pattern or Alarm Severity not included in message");
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "create relation",
@ -63,7 +61,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"Output connections: <code>Success</code> - if the relation already exists or successfully created, otherwise <code>Failure</code>.",
configDirective = "tbActionNodeCreateRelationConfig",
icon = "add_circle",
version = 1
version = 1,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/"
)
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -32,8 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "delete relation",
@ -55,7 +52,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"Output connections: <code>Success</code> - If the relation(s) successfully deleted, otherwise <code>Failure</code>.",
configDirective = "tbActionNodeDeleteRelationConfig",
icon = "remove_circle",
version = 1
version = 1,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/"
)
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java

@ -58,7 +58,8 @@ import java.util.Set;
"This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.",
configClazz = TbDeviceStateNodeConfiguration.class,
relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, "Rate limited"},
configDirective = "tbActionNodeDeviceStateConfig"
configDirective = "tbActionNodeDeviceStateConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-state/"
)
public class TbDeviceStateNode implements TbNode {

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
@ -44,19 +45,19 @@ import java.util.Objects;
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
configDirective = "tbActionNodeLogConfig",
icon = "menu"
icon = "menu",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/log/"
)
public class TbLogNode implements TbNode {
private TbLogNodeConfiguration config;
private ScriptEngine scriptEngine;
private boolean standard;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
this.standard = isStandard(config);
this.scriptEngine = this.standard ? null : createScriptEngine(ctx, config);
var config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
standard = isStandard(config);
scriptEngine = standard ? null : createScriptEngine(ctx, config);
}
ScriptEngine createScriptEngine(TbContext ctx, TbLogNodeConfiguration config) {
@ -75,7 +76,7 @@ public class TbLogNode implements TbNode {
return;
}
Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback<String>() {
Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable String result) {
log.info(result);
@ -83,7 +84,7 @@ public class TbLogNode implements TbNode {
}
@Override
public void onFailure(Throwable t) {
public void onFailure(@NonNull Throwable t) {
ctx.tellFailure(msg, t);
}
}, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor
@ -120,4 +121,5 @@ public class TbLogNode implements TbNode {
scriptEngine.destroy();
}
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -34,7 +33,6 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "message count",
@ -42,7 +40,8 @@ import java.util.concurrent.atomic.AtomicLong;
nodeDescription = "Count incoming messages",
nodeDetails = "Count incoming messages for specified interval and produces POST_TELEMETRY_REQUEST msg with messages count",
icon = "functions",
configDirective = "tbActionNodeMsgCountConfig"
configDirective = "tbActionNodeMsgCountConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/message-count/"
)
public class TbMsgCountNode implements TbNode {
@ -59,7 +58,6 @@ public class TbMsgCountNode implements TbNode {
this.delay = TimeUnit.SECONDS.toMillis(config.getInterval());
this.telemetryPrefix = config.getTelemetryPrefix();
scheduleTickMsg(ctx, null);
}
@Override

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java

@ -50,7 +50,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(type = ComponentType.ACTION,
@RuleNode(
type = ComponentType.ACTION,
name = "save to custom table",
configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class,
version = 1,
@ -64,7 +65,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
" otherwise, the message will be routed via <b>success</b> chain.",
configDirective = "tbActionNodeCustomTableConfig",
icon = "file_upload",
ruleChainTypes = RuleChainType.CORE)
ruleChainTypes = RuleChainType.CORE,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/"
)
public class TbSaveToCustomCassandraTableNode implements TbNode {
private static final String TABLE_PREFIX = "cs_tb_";

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java

@ -44,7 +44,8 @@ import org.thingsboard.server.common.msg.TbMsg;
"Other entities can be assigned only to one customer, so specified customer title in the configuration will be ignored if the originator isn't a dashboard.",
configDirective = "tbActionNodeUnAssignToCustomerConfig",
icon = "remove_circle",
version = 1
version = 1,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/"
)
public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode<TbUnassignFromCustomerNodeConfiguration> {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java

@ -92,8 +92,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie
configClazz = TbAiNodeConfiguration.class,
configDirective = "tbExternalNodeAiConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/external-nodes/#ai-request-node",
ruleChainTypes = RuleChainType.CORE
ruleChainTypes = RuleChainType.CORE,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/"
)
public final class TbAiNode extends TbAbstractExternalNode implements TbNode {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java

@ -54,7 +54,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie
"The node uses a pre-configured client and specified function to run.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbExternalNodeLambdaConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/"
)
public class TbAwsLambdaNode extends TbAbstractExternalNode {
@ -156,4 +157,5 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode {
}
}
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -47,7 +47,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"(<code>messageId</code>, <code>requestId</code>) in the Message Metadata from the AWS SNS. " +
"For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
configDirective = "tbExternalNodeSnsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/"
)
public class TbSnsNode extends TbAbstractExternalNode {
@ -125,4 +126,5 @@ public class TbSnsNode extends TbAbstractExternalNode {
}
}
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java

@ -53,7 +53,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
", <code>sequenceNumber</code>) in the Message Metadata from the AWS SQS." +
" For example <b>requestId</b> field can be accessed with <code>metadata.requestId</code>.",
configDirective = "tbExternalNodeSqsConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/"
)
public class TbSqsNode extends TbAbstractExternalNode {
@ -156,4 +157,5 @@ public class TbSqsNode extends TbAbstractExternalNode {
}
}
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -63,9 +63,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.",
inEnabled = false,
configDirective = "tbActionNodeGeneratorConfig",
icon = "repeat"
icon = "repeat",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/generator/"
)
public class TbMsgGeneratorNode implements TbNode {
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, EntityType.ENTITY_VIEW,

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java

@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "deduplication",
@ -59,9 +60,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
"<li><strong>ALL</strong> - return all messages as a single JSON array message. " +
"Where each element represents object with <strong><i>msg</i></strong> and <strong><i>metadata</i></strong> inner properties.</li></ul>",
icon = "content_copy",
configDirective = "tbTransformationNodeDeduplicationConfig"
configDirective = "tbTransformationNodeDeduplicationConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/"
)
@Slf4j
public class TbMsgDeduplicationNode implements TbNode {
public static final long TB_MSG_DEDUPLICATION_RETRY_DELAY = 10L;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.delay;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -34,7 +33,6 @@ import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "delay (deprecated)",
@ -45,7 +43,8 @@ import java.util.concurrent.TimeUnit;
"Deprecated because the acknowledged message still stays in memory (to be delayed) and this " +
"does not guarantee that message will be processed even if the \"retry failures and timeouts\" processing strategy will be chosen.",
icon = "pause",
configDirective = "tbActionNodeMsgDelayConfig"
configDirective = "tbActionNodeMsgDelayConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delay/"
)
public class TbMsgDelayNode implements TbNode {
@ -109,4 +108,5 @@ public class TbMsgDelayNode implements TbNode {
public void destroy() {
pendingMsgs.clear();
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java

@ -16,7 +16,6 @@
package org.thingsboard.rule.engine.edge;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.EntityType;
@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "push to cloud",
@ -48,7 +46,8 @@ import java.util.UUID;
"In case successful storage cloud event to database message will be routed via <b>Success</b> route.",
configDirective = "tbActionNodePushToCloudConfig",
icon = "cloud_upload",
ruleChainTypes = RuleChainType.EDGE
ruleChainTypes = RuleChainType.EDGE,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/"
)
public class TbMsgPushToCloudNode extends AbstractTbMsgPushNode<TbMsgPushToCloudNodeConfiguration, Object, Object> {
@ -80,7 +79,6 @@ public class TbMsgPushToCloudNode extends AbstractTbMsgPushNode<TbMsgPushToCloud
}
@Override
void processMsg(TbContext ctx, TbMsg msg) {
}
void processMsg(TbContext ctx, TbMsg msg) {}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNode.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -63,7 +64,8 @@ import static org.thingsboard.server.dao.edge.BaseRelatedEdgesService.RELATED_ED
"In case successful storage edge event to database message will be routed via <b>Success</b> route.",
configDirective = "tbActionNodePushToEdgeConfig",
icon = "cloud_download",
ruleChainTypes = RuleChainType.CORE
ruleChainTypes = RuleChainType.CORE,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/"
)
public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode<TbMsgPushToEdgeNodeConfiguration, EdgeEvent, EdgeEventType> {
@ -113,7 +115,7 @@ public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode<TbMsgPushToEdgeNo
}
@Override
public void onFailure(Throwable t) {
public void onFailure(@NonNull Throwable t) {
ctx.tellFailure(msg, t);
}
};

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "asset profile switch",
@ -36,7 +34,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Route incoming messages based on the name of the asset profile",
nodeDetails = "Route incoming messages based on the name of the asset profile. The asset profile name is case-sensitive.<br><br>" +
"Output connections: <i>Asset profile name</i> or <code>Failure</code>",
configDirective = "tbNodeEmptyConfig")
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/"
)
public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode {
@Override

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java

@ -43,7 +43,9 @@ import java.util.Objects;
nodeDescription = "Checks alarm status.",
nodeDetails = "Checks the alarm status to match one of the specified statuses.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>.",
configDirective = "tbFilterNodeCheckAlarmStatusConfig")
configDirective = "tbFilterNodeCheckAlarmStatusConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/"
)
public class TbCheckAlarmStatusNode implements TbNode {
private TbCheckAlarmStatusNodeConfig config;

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java

@ -16,7 +16,6 @@
package org.thingsboard.rule.engine.filter;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
import java.util.Map;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "check fields presence",
@ -40,7 +38,9 @@ import java.util.Map;
nodeDetails = "By default, the rule node checks that all specified fields are present. " +
"Uncheck the 'Check that all selected fields are present' if the presence of at least one field is sufficient.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeCheckMessageConfig")
configDirective = "tbFilterNodeCheckMessageConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/"
)
public class TbCheckMessageNode implements TbNode {
private static final Gson gson = new Gson();
@ -127,4 +127,4 @@ public class TbCheckMessageNode implements TbNode {
return (Map<String, String>) gson.fromJson(msg.getData(), Map.class);
}
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -41,10 +40,6 @@ import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "check relation presence",
@ -56,7 +51,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"Otherwise, the rule node checks the presence of a relation to any entity. " +
"In both cases, relation lookup is based on configured direction and type.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeCheckRelationConfig")
configDirective = "tbFilterNodeCheckRelationConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/"
)
public class TbCheckRelationNode implements TbNode {
private TbCheckRelationNodeConfiguration config;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "device profile switch",
@ -36,7 +34,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Route incoming messages based on the name of the device profile",
nodeDetails = "Route incoming messages based on the name of the device profile. The device profile name is case-sensitive<br><br>" +
"Output connections: <i>Device profile name</i> or <code>Failure</code>",
configDirective = "tbNodeEmptyConfig")
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/"
)
public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode {
@Override

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "script",
@ -44,18 +42,17 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
"Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
"Message type can be accessed via <code>msgType</code> property.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeScriptConfig"
configDirective = "tbFilterNodeScriptConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/script/"
)
public class TbJsFilterNode implements TbNode {
private TbJsFilterNodeConfiguration config;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
var config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
}
@Override
@ -75,4 +72,5 @@ public class TbJsFilterNode implements TbNode {
scriptEngine.destroy();
}
}
}

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.filter;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.ScriptEngine;
@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.Set;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "switch", customRelations = true,
@ -46,17 +44,17 @@ import java.util.Set;
"Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
"Message type can be accessed via <code>msgType</code> property.<br><br>" +
"Output connections: <i>Custom connection(s) defined by switch node</i> or <code>Failure</code>",
configDirective = "tbFilterNodeSwitchConfig")
configDirective = "tbFilterNodeSwitchConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/switch/"
)
public class TbJsSwitchNode implements TbNode {
private TbJsSwitchNodeConfiguration config;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
var config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
}
@Override
@ -84,4 +82,5 @@ public class TbJsSwitchNode implements TbNode {
scriptEngine.destroy();
}
}
}

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -26,10 +25,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "message type filter",
@ -38,14 +33,16 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDescription = "Filter incoming messages by Message Type",
nodeDetails = "If incoming message type is expected - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeMessageTypeConfig")
configDirective = "tbFilterNodeMessageTypeConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/"
)
public class TbMsgTypeFilterNode implements TbNode {
TbMsgTypeFilterNodeConfiguration config;
private TbMsgTypeFilterNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
}
@Override

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java

@ -15,18 +15,14 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "message type switch",
@ -36,15 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDetails = "Sends messages with message types <b>\"Post attributes\", \"Post telemetry\", \"RPC Request\"</b>" +
" etc. via corresponding chain, otherwise <b>Other</b> chain is used.<br><br>" +
"Output connections: <i>Message type connection</i>, <code>Other</code> - if message type is custom or <code>Failure</code>",
configDirective = "tbNodeEmptyConfig")
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/"
)
public class TbMsgTypeSwitchNode implements TbNode {
EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "entity type filter",
@ -36,14 +34,16 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDescription = "Filter incoming messages by the type of message originator entity",
nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeOriginatorTypeConfig")
configDirective = "tbFilterNodeOriginatorTypeConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/"
)
public class TbOriginatorTypeFilterNode implements TbNode {
TbOriginatorTypeFilterNodeConfiguration config;
private TbOriginatorTypeFilterNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class);
}
@Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java

@ -15,14 +15,12 @@
*/
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "entity type switch",
@ -31,7 +29,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
nodeDescription = "Route incoming messages by Message Originator Type",
nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).<br><br>" +
"Output connections: <i>Message originator type</i> or <code>Failure</code>",
configDirective = "tbNodeEmptyConfig")
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/"
)
public class TbOriginatorTypeSwitchNode extends TbAbstractTypeSwitchNode {
@Override

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java

@ -15,34 +15,27 @@
*/
package org.thingsboard.rule.engine.flow;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.FLOW,
name = "acknowledge",
configClazz = EmptyNodeConfiguration.class,
nodeDescription = "Acknowledges the incoming message",
nodeDetails = "After acknowledgement, the message is pushed to related rule nodes. Useful if you don't care what happens to this message next.",
configDirective = "tbNodeEmptyConfig"
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/"
)
public class TbAckNode implements TbNode {
EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -31,7 +30,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
@Slf4j
@RuleNode(
type = ComponentType.FLOW,
name = "checkpoint",
@ -40,7 +38,8 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME;
hasQueueName = true,
nodeDescription = "transfers the message to another queue",
nodeDetails = "After successful transfer incoming message is automatically acknowledged. Queue name is configurable.",
configDirective = "tbNodeEmptyConfig"
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/"
)
public class TbCheckpointNode implements TbNode {
@ -48,7 +47,7 @@ public class TbCheckpointNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.queueName = ctx.getQueueName();
queueName = ctx.getQueueName();
}
@Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RuleNode(
type = ComponentType.FLOW,
name = "rule chain",
@ -49,7 +47,8 @@ import java.util.UUID;
configDirective = "tbFlowNodeRuleChainInputConfig",
relationTypes = {},
ruleChainNode = true,
customRelations = true
customRelations = true,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/"
)
public class TbRuleChainInputNode implements TbNode {
@ -106,4 +105,5 @@ public class TbRuleChainInputNode implements TbNode {
default -> null;
});
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java

@ -15,17 +15,14 @@
*/
package org.thingsboard.rule.engine.flow;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.FLOW,
name = "output",
@ -35,13 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg;
"The output is forwarded to the caller rule chain, as an output of the corresponding \"input\" rule node. " +
"The output rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain. ",
configDirective = "tbFlowNodeRuleChainOutputConfig",
outEnabled = false
outEnabled = false,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/output/"
)
public class TbRuleChainOutputNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java

@ -53,7 +53,8 @@ import java.util.concurrent.TimeUnit;
"(<code>messageId</code> in the Message Metadata from the GCP PubSub. " +
"<b>messageId</b> field can be accessed with <code>metadata.messageId</code>.",
configDirective = "tbExternalNodePubSubConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo="
iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/"
)
public class TbPubSubNode extends TbAbstractExternalNode {
@ -155,4 +156,5 @@ public class TbPubSubNode extends TbAbstractExternalNode {
.setExecutorProvider(FixedExecutorProvider.create(ctx.getPubSubRuleNodeExecutorProvider().getExecutor()))
.build();
}
}

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java

@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -34,10 +33,10 @@ import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -47,10 +46,6 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT;
import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "gps geofencing events",
@ -66,12 +61,13 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE;
"If the presence monitoring strategy <b>\"On each message\"</b> is selected, sends messages via rule node connection type <code>Inside</code> or <code>Outside</code> every time the geofencing condition is satisfied. " +
"<br><br>" +
"Output connections: <code>Entered</code>, <code>Left</code>, <code>Inside</code>, <code>Outside</code>, <code>Success</code>",
configDirective = "tbActionNodeGpsGeofencingConfig"
configDirective = "tbActionNodeGpsGeofencingConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/"
)
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> {
private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage";
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>();
private final ConcurrentMap<EntityId, EntityGeofencingState> entityStates = new ConcurrentHashMap<>();
private final Gson gson = new Gson();
@Override

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.geo;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
@ -23,10 +22,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(
type = ComponentType.FILTER,
name = "gps geofencing filter",
@ -60,7 +55,9 @@ import org.thingsboard.server.common.msg.TbMsg;
"</br></br>" +
"Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;<br><br>" +
"Output connections: <code>True</code>, <code>False</code>, <code>Failure</code>",
configDirective = "tbFilterNodeGpsGeofencingConfig")
configDirective = "tbFilterNodeGpsGeofencingConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/"
)
public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofencingFilterNodeConfiguration> {
@Override
@ -72,4 +69,5 @@ public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofe
protected Class<TbGpsGeofencingFilterNodeConfiguration> getConfigClazz() {
return TbGpsGeofencingFilterNodeConfiguration.class;
}
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java

@ -58,7 +58,8 @@ import java.util.Properties;
"Outbound message will contain response fields (<code>offset</code>, <code>partition</code>, <code>topic</code>)" +
" from the Kafka in the Message Metadata. For example <b>partition</b> field can be accessed with <code>metadata.partition</code>.",
configDirective = "tbExternalNodeKafkaConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg=="
iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/kafka/"
)
public class TbKafkaNode extends TbAbstractExternalNode {

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java

@ -16,7 +16,6 @@
package org.thingsboard.rule.engine.mail;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "to email",
@ -43,7 +41,8 @@ import java.util.Map;
nodeDetails = "Transforms message to email message. If transformation completed successfully output message type will be set to <code>SEND_EMAIL</code>.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeToEmailConfig",
icon = "email"
icon = "email",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/"
)
public class TbMsgToEmailNode implements TbNode {
@ -55,20 +54,15 @@ public class TbMsgToEmailNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
this.dynamicMailBodyType = DYNAMIC.equals(this.config.getMailBodyType());
}
config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
dynamicMailBodyType = DYNAMIC.equals(config.getMailBodyType());
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
TbEmail email = convert(msg);
TbMsg emailMsg = buildEmailMsg(ctx, msg, email);
ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS);
} catch (Exception ex) {
log.warn("Can not convert message to email " + ex.getMessage());
ctx.tellFailure(msg, ex);
}
TbEmail email = convert(msg);
TbMsg emailMsg = buildEmailMsg(ctx, msg, email);
ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS);
}
private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) {

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -45,7 +45,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
" where created using <code>to Email</code> transformation Node, please connect this Node " +
"with <code>to Email</code> Node using <code>Successful</code> chain.",
configDirective = "tbExternalNodeSendEmailConfig",
icon = "send"
icon = "send",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-email/"
)
public class TbSendEmailNode extends TbAbstractExternalNode {
@ -91,7 +92,7 @@ public class TbSendEmailNode extends TbAbstractExternalNode {
}
}
private TbEmail getEmail(TbMsg msg) throws IOException {
private TbEmail getEmail(TbMsg msg) {
TbEmail email = JacksonUtil.fromString(msg.getData(), TbEmail.class);
if (StringUtils.isBlank(email.getTo())) {
throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
@ -141,4 +142,5 @@ public class TbSendEmailNode extends TbAbstractExternalNode {
}
return javaMailProperties;
}
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java

@ -20,7 +20,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import net.objecthunter.exp4j.Expression;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.ExpressionUtils;
@ -55,8 +54,6 @@ import java.util.stream.Collectors;
import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "math function",
@ -77,8 +74,8 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
"The execution is synchronized in scope of message originator (e.g. device) and server node. " +
"If you have rule nodes in different rule chains, they will process messages from the same originator synchronously in the scope of the server node.",
configDirective = "tbActionNodeMathFunctionConfig",
icon = "calculate"
icon = "calculate",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/math-function/"
)
public class TbMathNode implements TbNode {

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java

@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
@ -43,8 +42,8 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "calculate delta",
version = 1,
relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER},
@ -53,7 +52,9 @@ import java.util.Map;
"and current value for this key from the incoming message",
nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.<br><br>" +
"Output connections: <code>Success</code>, <code>Other</code> or <code>Failure</code>.",
configDirective = "tbEnrichmentNodeCalculateDeltaConfig")
configDirective = "tbEnrichmentNodeCalculateDeltaConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/"
)
public class CalculateDeltaNode implements TbNode {
private Map<EntityId, ValueWithTs> cache;
@ -189,7 +190,6 @@ public class CalculateDeltaNode implements TbNode {
return fetchLatestValueAsync(ctx, originator);
}
private record ValueWithTs(long ts, double value) {
}
private record ValueWithTs(long ts, double value) {}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java

@ -16,7 +16,6 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.concurrent.ExecutionException;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "fetch device credentials",
@ -45,7 +43,9 @@ import java.util.concurrent.ExecutionException;
"Useful when you need to fetch device credentials and use them for further message processing. " +
"For example, use device credentials to interact with external systems.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig")
configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/"
)
public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo<TbFetchDeviceCredentialsNodeConfiguration> {
private static final String CREDENTIALS = "credentials";

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -30,11 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "originator attributes",
configClazz = TbGetAttributesNodeConfiguration.class,
version = 1,
@ -43,7 +39,9 @@ import org.thingsboard.server.common.msg.TbMsg;
"that are not included in the incoming message to use them for further message processing. " +
"For example to filter messages based on the threshold value stored in the attributes.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/"
)
public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttributesNodeConfiguration, EntityId> {
@Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -30,7 +29,6 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "customer attributes",
@ -41,7 +39,9 @@ import org.thingsboard.server.common.data.util.TbPair;
"that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " +
"or actions such as alarm creation if the threshold is exceeded.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
configDirective = "tbEnrichmentNodeCustomerAttributesConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/"
)
public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<CustomerId> {
private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s";

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -41,8 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.NoSuchElementException;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "customer details",
configClazz = TbGetCustomerDetailsNodeConfiguration.class,
version = 1,
@ -50,7 +49,9 @@ import java.util.NoSuchElementException;
nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " +
"such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
configDirective = "tbEnrichmentNodeEntityDetailsConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/"
)
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration, CustomerId> {
private static final String CUSTOMER_PREFIX = "customer_";

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -31,8 +30,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "related device attributes",
configClazz = TbGetDeviceAttrNodeConfiguration.class,
version = 1,
@ -42,7 +41,9 @@ import org.thingsboard.server.common.msg.TbMsg;
"Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation " +
"to the message originator and use them for further message processing.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeDeviceAttributesConfig")
configDirective = "tbEnrichmentNodeDeviceAttributesConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/"
)
public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDeviceAttrNodeConfiguration, DeviceId> {
private static final String RELATED_DEVICE_NOT_FOUND_MESSAGE = "Failed to find related device to message originator using relation query specified in the configuration!";

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -31,11 +30,8 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.concurrent.ExecutionException;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "originator fields",
configClazz = TbGetOriginatorFieldsConfiguration.class,
version = 1,
@ -43,7 +39,9 @@ import java.util.concurrent.ExecutionException;
nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " +
"Useful when you need to retrieve originator fields and use them for further message processing.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeOriginatorFieldsConfig")
configDirective = "tbEnrichmentNodeOriginatorFieldsConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/"
)
public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode<EntityId, TbGetOriginatorFieldsConfiguration> {
protected final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping";

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -31,7 +30,6 @@ import org.thingsboard.server.common.data.util.TbPair;
import java.util.Arrays;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "related entity data",
@ -42,7 +40,9 @@ import java.util.Arrays;
"If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " +
"Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
configDirective = "tbEnrichmentNodeRelatedAttributesConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/"
)
public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode<EntityId> {
private static final String RELATED_ENTITY_NOT_FOUND_MESSAGE = "Failed to find related entity to message originator using relation query specified in the configuration!";

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java

@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
@ -45,11 +44,8 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Created by mshvayka on 04.09.18.
*/
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "originator telemetry",
configClazz = TbGetTelemetryNodeConfiguration.class,
version = 2,
@ -58,7 +54,9 @@ import java.util.stream.Collectors;
"instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " +
"Also, this node can be used for telemetry aggregation within configured fetch interval.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/"
)
public class TbGetTelemetryNode implements TbNode {
private TbGetTelemetryNodeConfiguration config;

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "tenant attributes",
@ -39,7 +37,9 @@ import org.thingsboard.server.common.data.util.TbPair;
nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " +
"that is stored as tenant attributes or telemetry data and use it for further message processing.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeTenantAttributesConfig")
configDirective = "tbEnrichmentNodeTenantAttributesConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/"
)
public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode<TenantId> {
@Override

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -30,8 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "tenant details",
configClazz = TbGetTenantDetailsNodeConfiguration.class,
version = 1,
@ -39,7 +38,9 @@ import org.thingsboard.server.common.msg.TbMsg;
nodeDetails = "Useful when we need to retrieve contact information from your tenant " +
"such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
configDirective = "tbEnrichmentNodeEntityDetailsConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/"
)
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration, TenantId> {
private static final String TENANT_PREFIX = "tenant_";

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -22,7 +22,6 @@ import io.netty.handler.codec.mqtt.MqttQoS;
import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
@ -49,7 +48,6 @@ import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "mqtt",
@ -59,7 +57,8 @@ import java.util.concurrent.TimeoutException;
nodeDescription = "Publish messages to the MQTT broker",
nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",
configDirective = "tbExternalNodeMqttConfig",
icon = "call_split"
icon = "call_split",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/"
)
public class TbMqttNode extends TbAbstractExternalNode {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java

@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import io.netty.handler.codec.mqtt.MqttVersion;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.AzureIotHubUtil;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.util.TbPair;
import java.time.Clock;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "azure iot hub",
@ -48,7 +46,8 @@ import java.time.Clock;
clusteringMode = ComponentClusteringMode.SINGLETON,
nodeDescription = "Publish messages to the Azure IoT Hub",
nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS <b>AT_LEAST_ONCE</b>.",
configDirective = "tbExternalNodeAzureIotHubConfig"
configDirective = "tbExternalNodeAzureIotHubConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/"
)
public class TbAzureIotHubNode extends TbMqttNode {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

@ -42,7 +42,8 @@ import java.util.concurrent.ExecutionException;
nodeDescription = "Sends notification to targets using the template",
nodeDetails = "Will send notification to the specified targets using the template",
configDirective = "tbExternalNodeNotificationConfig",
icon = "notifications"
icon = "notifications",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/"
)
public class TbNotificationNode extends TbAbstractExternalNode {
@ -51,7 +52,7 @@ public class TbNotificationNode extends TbAbstractExternalNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class);
}
@Override

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java

@ -34,7 +34,8 @@ import java.util.concurrent.ExecutionException;
nodeDescription = "Send message via Slack",
nodeDetails = "Sends message to a Slack channel or user",
configDirective = "tbExternalNodeSlackConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+"
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/"
)
public class TbSlackNode extends TbAbstractExternalNode {
@ -43,7 +44,7 @@ public class TbSlackNode extends TbAbstractExternalNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
super.init(ctx);
this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class);
}
@Override

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -59,7 +59,8 @@ import java.util.concurrent.TimeUnit;
nodeDescription = "Process device messages based on device profile settings",
nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " +
"'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.",
configDirective = "tbActionNodeDeviceProfileConfig"
configDirective = "tbActionNodeDeviceProfileConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/"
)
public class TbDeviceProfileNode implements TbNode {
@ -137,7 +138,7 @@ public class TbDeviceProfileNode implements TbNode {
if (deviceState != null) {
deviceState.process(ctx, msg);
} else {
log.info("Device was not found! Most probably device [" + deviceId + "] has been removed from the database. Acknowledging msg.");
log.info("Device was not found! Most probably device [{}] has been removed from the database. Acknowledging msg.", deviceId);
ctx.ack(msg);
}
}
@ -160,7 +161,7 @@ public class TbDeviceProfileNode implements TbNode {
deviceStates.clear();
}
protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) {
private DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) {
DeviceState deviceState = deviceStates.get(deviceId);
if (deviceState == null) {
DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId);

35
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java

@ -46,7 +46,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
nodeDescription = "Publish messages to the RabbitMQ",
nodeDetails = "Will publish message payload to RabbitMQ queue.",
configDirective = "tbExternalNodeRabbitMqConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4="
iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/"
)
public class TbRabbitMqNode extends TbAbstractExternalNode {
@ -99,10 +100,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode {
}
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {
return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg));
return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg));
}
private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception {
private TbMsg publishMessage(TbMsg msg) throws Exception {
String exchangeName = "";
if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) {
exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg);
@ -143,23 +144,15 @@ public class TbRabbitMqNode extends TbAbstractExternalNode {
}
static AMQP.BasicProperties convert(String name) throws TbNodeException {
switch (name) {
case "BASIC":
return MessageProperties.BASIC;
case "TEXT_PLAIN":
return MessageProperties.TEXT_PLAIN;
case "MINIMAL_BASIC":
return MessageProperties.MINIMAL_BASIC;
case "MINIMAL_PERSISTENT_BASIC":
return MessageProperties.MINIMAL_PERSISTENT_BASIC;
case "PERSISTENT_BASIC":
return MessageProperties.PERSISTENT_BASIC;
case "PERSISTENT_TEXT_PLAIN":
return MessageProperties.PERSISTENT_TEXT_PLAIN;
default:
throw new TbNodeException("Undefined message properties type '" + name +
"'! Only " + supportedPropertiesStr + " message properties types are supported!");
}
return switch (name) {
case "BASIC" -> MessageProperties.BASIC;
case "TEXT_PLAIN" -> MessageProperties.TEXT_PLAIN;
case "MINIMAL_BASIC" -> MessageProperties.MINIMAL_BASIC;
case "MINIMAL_PERSISTENT_BASIC" -> MessageProperties.MINIMAL_PERSISTENT_BASIC;
case "PERSISTENT_BASIC" -> MessageProperties.PERSISTENT_BASIC;
case "PERSISTENT_TEXT_PLAIN" -> MessageProperties.PERSISTENT_TEXT_PLAIN;
default -> throw new TbNodeException("Undefined message properties type '" + name + "'! Only " + supportedPropertiesStr + " message properties types are supported!");
};
}
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.rest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "rest api call",
@ -46,7 +44,8 @@ import java.util.List;
"<br/><b>Note-</b> if you use system proxy properties, the next system proxy properties should be added: \"http.proxyHost\" and \"http.proxyPort\" or \"https.proxyHost\" and \"https.proxyPort\" or \"socksProxyHost\" and \"socksProxyPort\"," +
"and if your proxy with auth, the next ones should be added: \"tb.proxy.user\" and \"tb.proxy.password\" to the thingsboard.conf file.",
configDirective = "tbExternalNodeRestApiCallConfig",
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+"
iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/"
)
public class TbRestApiCallNode extends TbAbstractExternalNode {

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.rest;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "rest call reply",
@ -36,7 +34,8 @@ import java.util.UUID;
nodeDescription = "Sends reply to REST API call to rule engine",
nodeDetails = "Expects messages with any message type. Forwards incoming message as a reply to REST API call sent to rule engine.",
configDirective = "tbActionNodeSendRestApiCallReplyConfig",
icon = "call_merge"
icon = "call_merge",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/"
)
public class TbSendRestApiCallReplyNode implements TbNode {
@ -44,7 +43,7 @@ public class TbSendRestApiCallReplyNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class);
}
@Override
@ -62,4 +61,5 @@ public class TbSendRestApiCallReplyNode implements TbNode {
ctx.tellSuccess(msg);
}
}
}

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java

@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -41,7 +41,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "rpc call reply",
@ -49,7 +48,8 @@ import java.util.UUID;
nodeDescription = "Sends reply to RPC call from device",
nodeDetails = "Expects messages with any message type. Will forward message body to the device.",
configDirective = "tbActionNodeRpcReplyConfig",
icon = "call_merge"
icon = "call_merge",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/"
)
public class TbSendRPCReplyNode implements TbNode {
@ -57,7 +57,7 @@ public class TbSendRPCReplyNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class);
}
@Override
@ -103,7 +103,7 @@ public class TbSendRPCReplyNode implements TbNode {
body.put("requestId", requestIdStr);
body.put("response", msg.getData());
EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE,
EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body));
EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body));
ListenableFuture<Void> future = ctx.getEdgeEventService().saveAsync(edgeEvent);
Futures.addCallback(future, new FutureCallback<>() {
@Override
@ -112,9 +112,10 @@ public class TbSendRPCReplyNode implements TbNode {
}
@Override
public void onFailure(Throwable t) {
public void onFailure(@NonNull Throwable t) {
ctx.tellFailure(msg, t);
}
}, ctx.getDbCallbackExecutor());
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java

@ -20,7 +20,6 @@ import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -41,7 +40,6 @@ import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "rpc call request",
@ -50,17 +48,18 @@ import java.util.concurrent.TimeUnit;
nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes." +
"If the RPC call request is originated by REST API call from user, will forward the response to user immediately.",
configDirective = "tbActionNodeRpcRequestConfig",
icon = "call_made"
icon = "call_made",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/"
)
public class TbSendRPCRequestNode implements TbNode {
private Random random = new Random();
private Gson gson = new Gson();
private final Random random = new Random();
private final Gson gson = new Gson();
private TbSendRpcRequestNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class);
config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class);
}
@Override

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.sms;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.EXTERNAL,
name = "send sms",
@ -36,7 +34,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
nodeDescription = "Sends SMS message via SMS provider.",
nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.",
configDirective = "tbExternalNodeSendSmsConfig",
icon = "sms"
icon = "sms",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/"
)
public class TbSendSmsNode extends TbAbstractExternalNode {

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java

@ -16,16 +16,13 @@
package org.thingsboard.rule.engine.telemetry;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -41,7 +38,6 @@ import java.util.Map;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "calculated fields and alarm rules",
@ -52,16 +48,13 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE;
"This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " +
"fields or alarm rules independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.",
configDirective = "tbNodeEmptyConfig",
icon = "published_with_changes"
icon = "published_with_changes",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/"
)
public class TbCalculatedFieldsNode implements TbNode {
private EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java

@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
@ -56,7 +55,6 @@ import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_MET
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "save attributes",
@ -107,7 +105,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R
Output connections: <code>Success</code>, <code>Failure</code>.
""",
configDirective = "tbActionNodeAttributesConfig",
icon = "file_upload"
icon = "file_upload",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/"
)
public class TbMsgAttributesNode implements TbNode {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.telemetry;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.AttributesDeleteRequest;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -35,7 +34,6 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "delete attributes",
@ -46,7 +44,8 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE;
" rule node will send the \"Attributes Deleted\" event to the root chain of the message originator and " +
" send the incoming message via <b>Success</b> chain, otherwise, <b>Failure</b> chain is used.",
configDirective = "tbActionNodeDeleteAttributesConfig",
icon = "remove_circle"
icon = "remove_circle",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/"
)
public class TbMsgDeleteAttributesNode implements TbNode {
@ -55,8 +54,8 @@ public class TbMsgDeleteAttributesNode implements TbNode {
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class);
this.keys = config.getKeys();
config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class);
keys = config.getKeys();
}
@Override

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java

@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.telemetry;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -52,7 +51,6 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin
import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "save time series",
@ -103,7 +101,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE
""",
configDirective = "tbActionNodeTimeseriesConfig",
icon = "file_upload",
version = 1
version = 1,
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/"
)
public class TbMsgTimeseriesNode implements TbNode {

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -55,7 +55,8 @@ import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED;
"'Device', 'Asset', 'Entity View', 'Edge' or 'User'.</li></ul>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"
icon = "find_replace",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/"
)
public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOriginatorNodeConfiguration> {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java

@ -47,22 +47,22 @@ import java.util.stream.Collectors;
"Regular expressions can be used to define which keys-value pairs to copy. Any configured key not found in the source will be ignored.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeCopyKeysConfig",
icon = "content_copy"
icon = "content_copy",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/"
)
public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbCopyKeysNodeConfiguration config;
private TbMsgSource copyFrom;
private List<Pattern> compiledKeyPatterns;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class);
this.copyFrom = config.getCopyFrom();
var config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class);
copyFrom = config.getCopyFrom();
if (copyFrom == null) {
throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
}
@Override

12
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java

@ -47,22 +47,22 @@ import java.util.stream.Collectors;
"keys and/or regular expressions.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeDeleteKeysConfig",
icon = "remove_circle"
icon = "remove_circle",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/"
)
public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbDeleteKeysNodeConfiguration config;
private TbMsgSource deleteFrom;
private List<Pattern> compiledKeyPatterns;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class);
this.deleteFrom = config.getDeleteFrom();
var config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class);
deleteFrom = config.getDeleteFrom();
if (deleteFrom == null) {
throw new TbNodeException("DeleteFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
}
@Override
@ -76,7 +76,7 @@ public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
var mdKeysToDelete = metaDataMap.keySet()
.stream()
.filter(this::matches)
.collect(Collectors.toList());
.toList();
mdKeysToDelete.forEach(metaDataMap::remove);
metaDataCopy = new TbMsgMetaData(metaDataMap);
hasNoChanges = mdKeysToDelete.isEmpty();

21
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java

@ -19,7 +19,6 @@ import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
@ -32,7 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.concurrent.ExecutionException;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "json path",
@ -41,32 +39,32 @@ import java.util.concurrent.ExecutionException;
nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
icon = "functions",
configDirective = "tbTransformationNodeJsonPathConfig"
configDirective = "tbTransformationNodeJsonPathConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/"
)
public class TbJsonPathNode implements TbNode {
private TbJsonPathNodeConfiguration config;
private Configuration configurationJsonPath;
private JsonPath jsonPath;
private String jsonPathValue;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class);
this.jsonPathValue = config.getJsonPath();
if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) {
this.configurationJsonPath = Configuration.builder()
var config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class);
jsonPathValue = config.getJsonPath();
if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) {
configurationJsonPath = Configuration.builder()
.jsonProvider(new JacksonJsonNodeJsonProvider())
.build();
this.jsonPath = JsonPath.compile(config.getJsonPath());
jsonPath = JsonPath.compile(config.getJsonPath());
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) {
if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) {
try {
Object jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath);
Object jsonPathData = jsonPath.read(msg.getData(), configurationJsonPath);
ctx.tellSuccess(msg.transform()
.data(JacksonUtil.toString(jsonPathData))
.build());
@ -77,4 +75,5 @@ public class TbJsonPathNode implements TbNode {
ctx.tellSuccess(msg);
}
}
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java

@ -44,19 +44,19 @@ import java.util.concurrent.ExecutionException;
"If key to rename doesn't exist in the specified source (message or message metadata) it will be ignored.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeRenameKeysConfig",
icon = "find_replace"
icon = "find_replace",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/"
)
public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbRenameKeysNodeConfiguration config;
private Map<String, String> renameKeysMapping;
private TbMsgSource renameIn;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class);
this.renameIn = config.getRenameIn();
this.renameKeysMapping = config.getRenameKeysMapping();
var config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class);
renameIn = config.getRenameIn();
renameKeysMapping = config.getRenameKeysMapping();
if (renameIn == null) {
throw new TbNodeException("RenameIn can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}

13
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.EmptyNodeConfiguration;
import org.thingsboard.rule.engine.api.RuleNode;
@ -25,7 +24,6 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@ -34,7 +32,6 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import java.util.concurrent.ExecutionException;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "split array msg",
@ -44,16 +41,13 @@ import java.util.concurrent.ExecutionException;
"All outbound messages will have the same type and metadata as the original array message.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
icon = "content_copy",
configDirective = "tbNodeEmptyConfig"
configDirective = "tbNodeEmptyConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/"
)
public class TbSplitArrayMsgNode implements TbNode {
private EmptyNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class);
}
public void init(TbContext ctx, TbNodeConfiguration configuration) {}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
@ -89,4 +83,5 @@ public class TbSplitArrayMsgNode implements TbNode {
ctx.tellFailure(msg, new RuntimeException("Msg data is not a JSON Array!"));
}
}
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -41,7 +41,8 @@ import java.util.List;
"<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
"All fields in resulting object are optional and will be taken from original message if not specified.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
configDirective = "tbTransformationNodeScriptConfig"
configDirective = "tbTransformationNodeScriptConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/script/"
)
public class TbTransformMsgNode extends TbAbstractTransformNode<TbTransformMsgNodeConfiguration> {
@ -71,4 +72,5 @@ public class TbTransformMsgNode extends TbAbstractTransformNode<TbTransformMsgNo
scriptEngine.destroy();
}
}
}

9
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GpsGeofencingActionTestCase.java

@ -18,14 +18,14 @@ package org.thingsboard.rule.engine.geo;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Data
public class GpsGeofencingActionTestCase {
private EntityId entityId;
private Map<EntityId, EntityGeofencingState> entityStates;
private ConcurrentMap<EntityId, EntityGeofencingState> entityStates;
private boolean msgInside;
private boolean reportPresenceStatusOnEachMessage;
@ -33,7 +33,8 @@ public class GpsGeofencingActionTestCase {
this.entityId = entityId;
this.msgInside = msgInside;
this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage;
this.entityStates = new HashMap<>();
this.entityStates = new ConcurrentHashMap<>();
this.entityStates.put(entityId, entityGeofencingState);
}
}

8
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -160,12 +160,16 @@
[tenantId]="data.tenantId"
[entityName]="data.entityName"/>
<div class="tb-form-row space-between flex-1 columns-xs" [class.!hidden]="!isRelatedEntity">
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}">{{ 'calculated-fields.zone-group-refresh-interval' | translate }}</div>
<mat-slide-toggle class="mat-slide" formControlName="scheduledUpdateEnabled">
<div tb-hint-tooltip-icon="{{'calculated-fields.hint.zone-group-refresh-interval' | translate}}">
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
</div>
</mat-slide-toggle>
<div class="flex flex-row items-center justify-start gap-2">
<tb-time-unit-input required
inlineField
requiredText="{{ 'calculated-fields.hint.zone-group-refresh-interval-required' | translate }}"
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate }}"
minErrorText="{{ 'calculated-fields.hint.zone-group-refresh-interval-min' | translate: {min: minAllowedScheduledUpdateIntervalInSecForCF} }}"
[minTime]="minAllowedScheduledUpdateIntervalInSecForCF"
formControlName="scheduledUpdateInterval">
</tb-time-unit-input>

19
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -81,6 +81,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}),
arguments: this.fb.control({}),
zoneGroups: this.fb.control({}),
scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
expressionSCRIPT: [calculatedFieldDefaultScript],
@ -144,6 +145,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
this.applyDialogData();
this.observeTypeChanges();
this.observeZoneChanges();
this.observeScheduledUpdateEnabled();
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.data.entityName, this.data.entityId);
}
@ -248,6 +250,23 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
this.checkRelatedEntity(this.configFormGroup.get('zoneGroups').value);
}
private observeScheduledUpdateEnabled(): void {
this.configFormGroup.get('scheduledUpdateEnabled').valueChanges
.pipe(takeUntilDestroyed())
.subscribe((value: boolean) =>
this.checkScheduledUpdateEnabled(value)
);
this.checkScheduledUpdateEnabled(this.configFormGroup.get('scheduledUpdateEnabled').value);
}
private checkScheduledUpdateEnabled(value: boolean) {
if (value) {
this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false});
} else {
this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false});
}
}
private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) {
this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery);
}

149
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html

@ -80,12 +80,13 @@
@if (ArgumentEntityTypeParamsMap.has(entityType)) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
<tb-entity-autocomplete class="flex flex-1"
<tb-entity-autocomplete class="flex-1"
#entityAutocomplete
formControlName="id"
inlineField
appearance="outline"
[placeholder]="'action.set' | translate"
[required]="true"
required
[entityType]="ArgumentEntityTypeParamsMap.get(entityType).entityType"
(entityChanged)="entityNameSubject.next($event?.name)"/>
</div>
@ -94,76 +95,118 @@
<ng-container [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>{{ 'calculated-fields.relation-query' | translate }}*</mat-expansion-panel-header>
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.direction' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionTranslations.get(direction) | translate }}</mat-option>
<mat-expansion-panel-header>{{ 'calculated-fields.entity-zone-relationship' | translate }}</mat-expansion-panel-header>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-level</div>
<div class="tb-form-table-header-cell flex-1 tb-required" translate>calculated-fields.relation-type</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
@if (levelsFormArray()?.controls?.length) {
<div class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
@for (keyControl of levelsFormArray().controls; track trackByKey;) {
<div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
class="lt-lg:!hidden"
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<div class="tb-form-row no-border flex-1" [formGroup]="keyControl">
<div class="level-text">{{ $index+1 }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionLevelTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
matTooltip="{{ 'calculated-fields.delete-level' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-level' | translate }}</span>
}
@if (levelsFormArray().errors) {
<tb-error noMargin error="{{ 'calculated-fields.levels-required' | translate }}" style="padding-left: 12px;"></tb-error>
}
</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.relation-type' | translate }}</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
<div>
@if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.max-allowed-levels-error' | translate }}</span>
</div>
} @else {
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'calculated-fields.add-level' | translate }}
</button>
}
</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.relation-level' | translate }}</div>
</mat-expansion-panel>
</div>
</ng-container>
<ng-container>
@if (entityFilter.singleEntity.id) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
</div>
@if (entityType === ArgumentEntityType.RelationQuery) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" step="1" min="0" formControlName="maxLevel" placeholder="{{ 'action.set' | translate }}"/>
@if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.relation-level-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('min')) {
<input matInput autocomplete="new-name" name="value" formControlName="perimeterKeyName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.relation-level-min' | translate"
[matTooltip]="'calculated-fields.perimeter-attribute-key-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('max')) {
} @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.relation-level-max' | translate: {max: maxRelationLevelPerCfArgument}"
[matTooltip]="'calculated-fields.perimeter-attribute-key-pattern' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
<div class="tb-form-row" [class.!hidden]="!(this.refDynamicSourceFormGroup.get('maxLevel').value > 1)">
<mat-slide-toggle class="mat-slide margin" formControlName="fetchLastLevelOnly">
{{ 'calculated-fields.fetch-last-available-level' | translate }}
</mat-slide-toggle>
</div>
</mat-expansion-panel>
</div>
</ng-container>
<ng-container>
<div class="tb-form-row">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
} @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName" [dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter" [keyScopeType]="AttributeScope.SERVER_SCOPE"/>
}
</div>
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName" [dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter"/>
</div>
}
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'calculated-fields.hint.report-strategy' | translate}}">{{ 'calculated-fields.report-strategy' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

33
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss

@ -29,6 +29,34 @@ $panel-width: 520px;
}
}
.level-text {
display: flex;
justify-content: center;
width: 25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-form-table {
.tb-form-row {
gap: 12px;
}
.tb-form-table-body {
gap: unset;
}
.tb-form-table-header {
padding: 0;
}
.tb-form-table-header-cell {
&.tb-actions-header {
width: 40px;
min-width: 40px;
}
}
}
.limit-field-row {
@media screen and (max-width: $panel-width) {
display: flex;
@ -48,4 +76,9 @@ $panel-width: 520px;
flex-direction: column;
}
}
tb-entity-autocomplete {
.mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper {
padding-right: 0 !important;
}
}
}

79
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts

@ -16,7 +16,15 @@
import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
UntypedFormArray,
ValidatorFn,
Validators
} from '@angular/forms';
import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants';
import {
ArgumentEntityType,
@ -25,14 +33,15 @@ import {
CalculatedFieldGeofencing,
CalculatedFieldGeofencingValue,
CalculatedFieldType,
GeofencingDirectionLevelTranslations,
GeofencingDirectionTranslations,
GeofencingReportStrategy,
GeofencingReportStrategyTranslations,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
import { debounceTime, delay, distinctUntilChanged, filter, map } from 'rxjs/operators';
import { debounceTime, delay, distinctUntilChanged, map } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityFilter } from '@shared/models/query/query.models';
@ -44,6 +53,7 @@ import { Store } from '@ngrx/store';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntitySearchDirection } from '@shared/models/relation.models';
import { CdkDragDrop } from "@angular/cdk/drag-drop";
@Component({
selector: 'tb-calculated-field-geofencing-zone-groups-panel',
@ -73,12 +83,9 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
id: ['']
}),
refDynamicSourceConfiguration: this.fb.group({
direction: [EntitySearchDirection.TO],
relationType: ['', [Validators.required]],
maxLevel: [1, [Validators.required, Validators.min(1), Validators.max(this.maxRelationLevelPerCfArgument)]],
fetchLastLevelOnly: [false],
levels: this.fb.array([], [this.levelsRequired()])
}),
perimeterKeyName: ['', [Validators.pattern(oneSpaceInsideRegex)]],
perimeterKeyName: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex)]],
reportStrategy: [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS],
createRelationsWithMatchedZones: [false],
direction: [EntitySearchDirection.TO],
@ -97,6 +104,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations;
readonly GeofencingDirectionList = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>;
readonly GeofencingDirectionTranslations = GeofencingDirectionTranslations;
readonly GeofencingDirectionLevelTranslations = GeofencingDirectionLevelTranslations;
readonly AttributeScope = AttributeScope;
private currentEntityFilter: EntityFilter;
@ -107,7 +116,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
private store: Store<AppState>
) {
this.observeMaxLevelChanges();
this.observeEntityFilterChanges();
this.observeEntityTypeChanges();
this.observeUpdatePosition();
@ -131,7 +139,16 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
if (this.zone.refDynamicSourceConfiguration?.type) {
this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false});
}
this.validateFetchLastLevelOnly(this.zone?.refDynamicSourceConfiguration?.maxLevel);
if (this.zone?.refDynamicSourceConfiguration?.levels?.length > 0) {
this.zone.refDynamicSourceConfiguration.levels.forEach(level => {
this.levelsFormArray().push(this.fb.group({
direction: [level.direction],
relationType: [level.relationType, [Validators.required]]
}));
})
} else {
this.addKey();
}
this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones);
this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type);
@ -241,17 +258,19 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
private observeEntityFilterChanges(): void {
merge(
this.refEntityIdFormGroup.get('entityType').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)),
this.refEntityIdFormGroup.get('id').valueChanges,
)
.pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType));
this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset(''));
}
private observeEntityTypeChanges(): void {
this.refEntityIdFormGroup.get('entityType').valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => {
this.geofencingFormGroup.get('refEntityId').get('id').setValue('');
this.geofencingFormGroup.get('refEntityId').get('id').setValue(null);
const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery;
this.geofencingFormGroup.get('refEntityId')
.get('id')[isEntityWithId ? 'enable' : 'disable']();
@ -271,6 +290,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
};
}
private levelsRequired(): ValidatorFn {
return (control: FormControl) => {
return control.value.length ? null : { levelsRequired: true };
};
}
private forbiddenNameValidator(): ValidatorFn {
return (control: FormControl) => {
const trimmedValue = control.value.trim().toLowerCase();
@ -282,10 +307,40 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
private observeUpdatePosition(): void {
merge(
this.refEntityIdFormGroup.get('entityType').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges,
this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges
)
.pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition());
}
levelsFormArray(): UntypedFormArray {
return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray;
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
removeKey(index: number) {
this.levelsFormArray().removeAt(index);
}
addKey() {
this.levelsFormArray().push(this.fb.group({
direction: [EntitySearchDirection.TO],
relationType: ['', [Validators.required]]
}));
}
keyDrop(event: CdkDragDrop<string[]>) {
const keysArray = this.levelsFormArray();
const key = keysArray.at(event.previousIndex);
keysArray.removeAt(event.previousIndex);
keysArray.insert(event.currentIndex, key);
}
get dragEnabled(): boolean {
return this.levelsFormArray().controls.length > 1;
}
}

20
ui-ngx/src/app/modules/home/components/resources/resources-library.component.html

@ -30,7 +30,7 @@
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
(click)="onEntityAction($event, 'deleteResource')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'resource.delete' | translate }}
</button>
@ -48,16 +48,14 @@
<div class="mat-padding flex flex-col">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
@if (resourceTypes.length > 1) {
<mat-form-field class="mat-block">
<mat-label translate>resource.resource-type</mat-label>
<mat-select formControlName="resourceType" required>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
}
<mat-form-field class="mat-block">
<mat-label translate>resource.resource-type</mat-label>
<mat-select formControlName="resourceType" required>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<mat-label translate>resource.title</mat-label>
<input matInput formControlName="title" required>

4
ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts

@ -112,6 +112,10 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
if (this.isEdit && this.entityForm && !this.isAdd) {
this.entityForm.get('resourceType').disable({ emitEvent: false });
this.entityForm.get('fileName').disable({ emitEvent: false });
this.entityForm.get('data').disable({ emitEvent: false });
}
if (this.isAdd && this.resourceTypes.length === 1) {
this.entityForm.get('resourceType').disable({ emitEvent: false });
}
}

2
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -173,7 +173,7 @@ export class ResourcesLibraryTableConfigResolver {
case 'downloadResource':
this.downloadResource(action.event, action.entity);
return true;
case 'deleteLibrary':
case 'deleteResource':
this.deleteResource(action.event, action.entity);
}
return false;

1
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts

@ -133,6 +133,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
if (filterChanged || keyScopeChanged || keyTypeChanged) {
this.keyControl.setValue('', {emitEvent: false});
this.cachedResult = null;
}
}

14
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -73,7 +73,7 @@ export enum ArgumentEntityType {
Asset = 'ASSET',
Customer = 'CUSTOMER',
Tenant = 'TENANT',
RelationQuery = 'RELATION_QUERY',
RelationQuery = 'RELATION_PATH_QUERY',
}
export const ArgumentEntityTypeTranslations = new Map<ArgumentEntityType, string>(
@ -108,6 +108,13 @@ export const GeofencingDirectionTranslations = new Map<EntitySearchDirection, st
]
)
export const GeofencingDirectionLevelTranslations = new Map<EntitySearchDirection, string>(
[
[EntitySearchDirection.FROM, 'calculated-fields.direction-down'],
[EntitySearchDirection.TO, 'calculated-fields.direction-up'],
]
)
export enum ArgumentType {
Attribute = 'ATTRIBUTE',
LatestTelemetry = 'TS_LATEST',
@ -167,10 +174,7 @@ export interface CalculatedFieldGeofencing {
export interface RefDynamicSourceConfiguration {
type?: ArgumentEntityType.RelationQuery;
direction: EntitySearchDirection;
relationType: string;
maxLevel: number;
fetchLastLevelOnly?: boolean;
levels?: Array<{direction: EntitySearchDirection; relationType: string;}>;
}
export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing {

145
ui-ngx/src/app/shared/models/constants.ts

@ -94,76 +94,83 @@ export const HelpLinks = {
oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/',
oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog',
oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app',
oauth2Google: 'https://developers.google.com/google-ads/api/docs/start',
oauth2Google: 'https://developers.google.com/identity/protocols/oauth2',
ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`,
ruleNodeCheckRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node`,
ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node`,
ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#gps-geofencing-filter-node`,
ruleNodeJsFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node`,
ruleNodeJsSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#switch-node`,
ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#asset-profile-switch`,
ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#device-profile-switch`,
ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-alarm-status`,
ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node`,
ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node`,
ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node`,
ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node`,
ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes`,
ruleNodeOriginatorFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields`,
ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-telemetry`,
ruleNodeCustomerAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes`,
ruleNodeCustomerDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-details`,
ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#fetch-device-credentials`,
ruleNodeDeviceAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes`,
ruleNodeRelatedAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes`,
ruleNodeTenantAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes`,
ruleNodeTenantDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-details`,
ruleNodeChangeOriginator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#change-originator`,
ruleNodeTransformMsg: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node`,
ruleNodeMsgToEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node`,
ruleNodeAssignToCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#assign-to-customer-node`,
ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#unassign-from-customer-node`,
ruleNodeCalculatedFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#calculated-fields-node`,
ruleNodeClearAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node`,
ruleNodeCreateAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node`,
ruleNodeCopyToView: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#copy-to-view-node`,
ruleNodeCreateRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-relation-node`,
ruleNodeDeleteRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-relation-node`,
ruleNodeDeviceState: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-state-node`,
ruleNodeMessageCount: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#message-count-node`,
ruleNodeMsgDelay: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delay-node-deprecated`,
ruleNodeMsgGenerator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#generator-node`,
ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#gps-geofencing-events-node`,
ruleNodeLog: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#log-node`,
ruleNodeRpcCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node`,
ruleNodeRpcCallRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node`,
ruleNodeSaveAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node`,
ruleNodeDeleteAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-attributes-node`,
ruleNodeSaveTimeseries: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node`,
ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table-node`,
ruleNodeRuleChain: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node`,
ruleNodeOutputNode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#output-node`,
ruleNodeAiRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#ai-request-node`,
ruleNodeAwsLambda: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-lambda-node`,
ruleNodeAwsSns: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node`,
ruleNodeAwsSqs: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node`,
ruleNodeKafka: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#kafka-node`,
ruleNodeMqtt: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#mqtt-node`,
ruleNodeAzureIotHub: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#azure-iot-hub-node`,
ruleNodeRabbitMq: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node`,
ruleNodeRestApiCall: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node`,
ruleNodeSendEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-email-node`,
ruleNodeSendSms: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-sms-node`,
ruleNodeMath: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#math-function-node`,
ruleNodeCalculateDelta: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#calculate-delta`,
ruleNodeRestCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rest-call-reply-node`,
ruleNodePushToCloud: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud`,
ruleNodePushToEdge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-edge`,
ruleNodeDeviceProfile: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-profile-node`,
ruleNodeAcknowledge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#acknowledge-node`,
ruleNodeCheckpoint: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#checkpoint-node`,
ruleNodeSendNotification: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-notification-node`,
ruleNodeSendSlack: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-to-slack-node`,
ruleNodeCheckRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/`,
ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/`,
ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/`,
ruleNodeJsFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/script/`,
ruleNodeJsSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/switch/`,
ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/`,
ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/`,
ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/`,
ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/`,
ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/`,
ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/`,
ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/`,
ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/`,
ruleNodeOriginatorFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/`,
ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/`,
ruleNodeCustomerAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/`,
ruleNodeCustomerDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/`,
ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/`,
ruleNodeDeviceAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/`,
ruleNodeRelatedAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/`,
ruleNodeTenantAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/`,
ruleNodeTenantDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/`,
ruleNodeChangeOriginator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/`,
ruleNodeCopyKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/`,
ruleNodeDeduplication: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/`,
ruleNodeDeleteKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/`,
ruleNodeJsonPath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/`,
ruleNodeRenameKeys: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/`,
ruleNodeTransformMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/script/`,
ruleNodeSplitArrayMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/`,
ruleNodeMsgToEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/`,
ruleNodeAssignToCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/`,
ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/`,
ruleNodeCalculatedFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/`,
ruleNodeClearAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/`,
ruleNodeCreateAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/`,
ruleNodeCopyToView: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/copy-to-view/`,
ruleNodeCreateRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/`,
ruleNodeDeleteRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/`,
ruleNodeDeviceState: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-state/`,
ruleNodeMessageCount: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/message-count/`,
ruleNodeMsgDelay: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delay/`,
ruleNodeMsgGenerator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/generator/`,
ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/`,
ruleNodeLog: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/log/`,
ruleNodeRpcCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/`,
ruleNodeRpcCallRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/`,
ruleNodeSaveAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/`,
ruleNodeDeleteAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/`,
ruleNodeSaveTimeseries: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/`,
ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/`,
ruleNodeRuleChain: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/`,
ruleNodeOutputNode: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/output/`,
ruleNodeAiRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/`,
ruleNodeAwsLambda: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/`,
ruleNodeAwsSns: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/`,
ruleNodeAwsSqs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`,
ruleNodeKafka: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/kafka/`,
ruleNodeMqtt: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/`,
ruleNodeAzureIotHub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/`,
ruleNodeGcpPubSub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/`,
ruleNodeRabbitMq: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/`,
ruleNodeRestApiCall: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/`,
ruleNodeSendEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-email/`,
ruleNodeSendSms: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/`,
ruleNodeMath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/math-function/`,
ruleNodeCalculateDelta: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/`,
ruleNodeRestCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/`,
ruleNodePushToCloud: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/`,
ruleNodePushToEdge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/`,
ruleNodeDeviceProfile: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/`,
ruleNodeAcknowledge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/`,
ruleNodeCheckpoint: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/`,
ruleNodeSendNotification: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/`,
ruleNodeSendSlack: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`,
tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenants`,
tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenant-profiles`,
customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/customers`,

7
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -476,7 +476,13 @@ const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.metadata.TbGetTenantDetailsNode': 'ruleNodeTenantDetails',
'org.thingsboard.rule.engine.metadata.CalculateDeltaNode': 'ruleNodeCalculateDelta',
'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator',
'org.thingsboard.rule.engine.transform.TbCopyKeysNode': 'ruleNodeCopyKeyValuePairs',
'org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode': 'ruleNodeDeduplication',
'org.thingsboard.rule.engine.transform.TbDeleteKeysNode': 'ruleNodeDeleteKeyValuePairs',
'org.thingsboard.rule.engine.transform.TbJsonPathNode': 'ruleNodeJsonPath',
'org.thingsboard.rule.engine.transform.TbRenameKeysNode': 'ruleNodeRenameKeys',
'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg',
'org.thingsboard.rule.engine.transform.TbSplitArrayMsgNode': 'ruleNodeSplitArrayMsg',
'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail',
'org.thingsboard.rule.engine.action.TbAssignToCustomerNode': 'ruleNodeAssignToCustomer',
'org.thingsboard.rule.engine.action.TbUnassignFromCustomerNode': 'ruleNodeUnassignFromCustomer',
@ -505,6 +511,7 @@ const ruleNodeClazzHelpLinkMap = {
'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka',
'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt',
'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode': 'ruleNodeAzureIotHub',
'org.thingsboard.rule.engine.gcp.pubsub.TbPubSubNode': 'ruleNodeGcpPubSub',
'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq',
'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall',
'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail',

27
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1122,17 +1122,28 @@
"report-presence-status-only": "Presence status only",
"report-transition-event-and-presence": "Presence status and transition events",
"perimeter-attribute-key": "Perimeter attribute key",
"relation-query": "Relations query",
"direction": "Direction",
"direction-from": "From source entity",
"direction-to": "To source entity",
"perimeter-attribute-key-required": "Perimeter attribute key is required.",
"perimeter-attribute-key-pattern": "Perimeter attribute key is invalid.",
"entity-zone-relationship": "Path from Entity to Zones *",
"direction": "Relation direction",
"direction-from": "From entity to zone",
"direction-to": "From zone to entity",
"relation-type": "Relation type",
"create-relation-with-matched-zones": "Create relations with matched zones",
"create-relation-with-matched-zones": "Create relations for source entity with matched zones",
"relation-level": "Relation level",
"fetch-last-available-level": "Fetch last available level only",
"zone-group-refresh-interval": "Zone groups refresh interval",
"copy-zone-group-name": "Copy zone group name",
"open-details-page": "Open entity details page",
"level": "Level",
"direction-level": "Direction",
"direction-up": "Up",
"direction-down": "Down",
"add-level": "Add level",
"delete-level": "Delete level",
"no-level": "No level configured",
"levels-required": "At least one level must be configured.",
"max-allowed-levels-error": "Relation level exceeds the maximum allowed.",
"hint": {
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.",
"arguments-empty": "Arguments should not be empty.",
@ -1158,7 +1169,7 @@
"entity-coordinates": "Specify the time series keys that provide entity GPS coordinates (latitude and longitude).",
"geofencing-zone-groups": "Define one or more geofencing zones groups to check (e.g. 'allowedZones', 'restrictedZones'). Each group must have a unique name, which is used as a prefix for calculated field output telemetry keys.",
"perimeter-attribute-key": "Set the attribute key that contains the geofencing zone perimeter definition. The perimeter is always taken from server-side attributes of the zone entity.",
"report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group.Transition events report when the entity ENTERED or LEFT the zone group.",
"report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group. Transition events report when the entity ENTERED or LEFT the zone group.",
"create-relation-with-matched-zones": "Automatically create and maintain relations between the entity and the zones it is currently inside. Relations are removed when the entity leaves a zone and created when it enters a new one.",
"relation-type-required": "Relation type is required.",
"relation-level-required": "Relation level is required.",
@ -1167,9 +1178,9 @@
"geofencing-empty": "At least one zone group must be configured.",
"geofencing-entity-not-found": "Geofencing target entity not found.",
"max-geofencing-zone": "Maximum number of geofencing zones reached.",
"zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed. Set to 0 to disable scheduled refresh.",
"zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.",
"zone-group-refresh-interval-required": "Zone groups refresh interval is required.",
"zone-group-refresh-interval-min": "Zone group refresh interval is below the minimum allowed system interval."
"zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second."
}
},
"ai-models": {

Loading…
Cancel
Save