Browse Source

Merge remote-tracking branch 'irynamatveieva/fix/propagation-cf' into feature/propagation-cf

pull/14208/head
dshvaika 11 months ago
parent
commit
996b5a84ec
  1. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 32
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 34
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 16
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  5. 14
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  6. 16
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  7. 27
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  8. 16
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  9. 5
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  10. 36
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  11. 20
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  12. 22
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  13. 7
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  14. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  15. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  16. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  18. 130
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
  19. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java
  20. 81
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
  21. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  22. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  23. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  24. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java
  26. 15
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  27. 5
      application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
  28. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java
  29. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java
  30. 7
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  31. 18
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  32. 9
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  33. 19
      application/src/main/java/org/thingsboard/server/utils/CsvUtils.java
  34. 107
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  35. 6
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  36. 24
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  37. 3
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  38. 22
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  39. 81
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  40. 25
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  41. 196
      application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java
  42. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  43. 4
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java
  44. 77
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  45. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
  46. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  47. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  48. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
  49. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  50. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  51. 23
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java
  52. 25
      common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java
  53. 23
      common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java
  54. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java
  55. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  56. 8
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java
  57. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java
  58. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java
  59. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java
  60. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java
  61. 35
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java
  62. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java
  63. 10
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java
  64. 3
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java
  66. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  67. 4
      common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java
  68. 29
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  69. 12
      common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
  70. 5
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java
  71. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  72. 9
      common/edge-api/src/main/proto/edge.proto
  73. 2
      common/proto/src/main/proto/queue.proto
  74. 5
      dao/src/main/java/org/thingsboard/server/dao/Dao.java
  75. 41
      dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java
  76. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  77. 20
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  78. 27
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  79. 33
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  80. 44
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  81. 20
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  82. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java
  83. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java
  84. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java
  85. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java
  86. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java
  87. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
  88. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
  89. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
  90. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  91. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  92. 25
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  93. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java
  94. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  95. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java
  96. 9
      ui-ngx/src/app/core/http/asset.service.ts
  97. 9
      ui-ngx/src/app/core/http/customer.service.ts
  98. 18
      ui-ngx/src/app/core/http/device.service.ts
  99. 9
      ui-ngx/src/app/core/http/entity-view.service.ts
  100. 34
      ui-ngx/src/app/core/http/http-utils.ts

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -860,8 +860,9 @@ public class ActorSystemContext {
if (errorMessage != null) { if (errorMessage != null) {
eventBuilder.error(errorMessage); eventBuilder.error(errorMessage);
} }
CalculatedFieldDebugEvent event = eventBuilder.build();
ListenableFuture<Void> future = eventService.saveAsync(eventBuilder.build()); log.debug("Persisting calculated field debug event: {}", event);
ListenableFuture<Void> future = eventService.saveAsync(event);
Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
log.warn("Failed to persist calculated field debug message", ex); log.warn("Failed to persist calculated field debug message", ex);

32
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -174,7 +174,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException { public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing CF argument reset msg.", entityId); log.debug("[{}] Processing CF argument reset msg.", entityId);
var ctx = msg.getCtx(); var ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback());
try { try {
Map<String, Argument> dynamicSourceArgs = ctx.getArguments().entrySet().stream() Map<String, Argument> dynamicSourceArgs = ctx.getArguments().entrySet().stream()
.filter(entry -> entry.getValue().hasOwnerSource()) .filter(entry -> entry.getValue().hasOwnerSource())
@ -183,7 +182,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs); Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs);
fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true));
processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), callback, fetchedArgs, null, null); processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, null);
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
@ -213,7 +212,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException {
log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg);
var proto = msg.getProto(); var proto = msg.getProto();
var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); var numberOfCallbacks = msg.getEntityIdFields().size() + msg.getProfileIdFields().size();
MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback());
List<CalculatedFieldId> cfIdList = getCalculatedFieldIds(proto); List<CalculatedFieldId> cfIdList = getCalculatedFieldIds(proto);
Set<CalculatedFieldId> cfIdSet = new HashSet<>(cfIdList); Set<CalculatedFieldId> cfIdSet = new HashSet<>(cfIdList);
@ -229,11 +228,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg);
var proto = msg.getProto(); var proto = msg.getProto();
var ctx = msg.getCtx(); var ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var callback = msg.getCallback();
try { try {
List<CalculatedFieldId> cfIds = getCalculatedFieldIds(proto); List<CalculatedFieldId> cfIds = getCalculatedFieldIds(proto);
if (cfIds.contains(ctx.getCfId())) { if (cfIds.contains(ctx.getCfId())) {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} else { } else {
if (proto.getTsDataCount() > 0) { if (proto.getTsDataCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
@ -244,7 +243,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else if (proto.getRemovedAttrKeysCount() > 0) { } else if (proto.getRemovedAttrKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -253,10 +252,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} }
private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection<CalculatedFieldId> cfIds, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection<CalculatedFieldId> cfIds, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
try { try {
if (cfIds.contains(ctx.getCfId())) { if (cfIds.contains(ctx.getCfId())) {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} else { } else {
if (proto.getTsDataCount() > 0) { if (proto.getTsDataCount() > 0) {
processTelemetry(ctx, proto, cfIdList, callback); processTelemetry(ctx, proto, cfIdList, callback);
@ -267,7 +266,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else if (proto.getRemovedAttrKeysCount() > 0) { } else if (proto.getRemovedAttrKeysCount() > 0) {
processRemovedAttributes(ctx, proto, cfIdList, callback); processRemovedAttributes(ctx, proto, cfIdList, callback);
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -307,27 +306,27 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
msg.getCallback().onSuccess(); msg.getCallback().onSuccess();
} }
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback, private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, TbCallback callback,
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException {
if (newArgValues.isEmpty()) { if (newArgValues.isEmpty()) {
log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); log.debug("[{}] No new argument values to process for CF.", ctx.getCfId());
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
CalculatedFieldState state = states.get(ctx.getCfId()); CalculatedFieldState state = states.get(ctx.getCfId());
boolean justRestored = false; boolean justRestored = false;
@ -354,7 +353,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
cfIdList.add(ctx.getCfId()); cfIdList.add(ctx.getCfId());
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback);
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} else { } else {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
@ -395,6 +394,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx, private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx,
List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback);
log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs);
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeChecked = false; boolean stateSizeChecked = false;

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

@ -121,7 +121,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void stop() { public void stop() {
log.info("[{}] Stopping CF manager actor.", tenantId); log.info("[{}] Stopping CF manager actor.", tenantId);
calculatedFields.values().forEach(CalculatedFieldCtx::stop); calculatedFields.values().forEach(CalculatedFieldCtx::close);
calculatedFields.clear(); calculatedFields.clear();
entityIdCalculatedFields.clear(); entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear(); entityIdCalculatedFieldLinks.clear();
@ -326,7 +326,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var newCfCtx = getCfCtx(newCf); // fixme wtf? why isn't oldCfCtx closed properly? when to close it? var newCfCtx = getCfCtx(newCf);
try { try {
newCfCtx.init(); newCfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -366,14 +366,26 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return; return;
} }
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb)); applyToTargetCfEntityActors(newCfCtx, new TbCallback() {
@Override
public void onSuccess() {
oldCfCtx.close();
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
oldCfCtx.close();
callback.onFailure(t);
}
}, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb));
} }
} }
} }
private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) { private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var cfCtx = calculatedFields.remove(cfId); // fixme wtf? why isn't ctx closed properly? var cfCtx = calculatedFields.remove(cfId);
if (cfCtx == null) { if (cfCtx == null) {
log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); log.debug("[{}] CF was already deleted [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
@ -381,7 +393,19 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx); deleteLinks(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); applyToTargetCfEntityActors(cfCtx, new TbCallback() {
@Override
public void onSuccess() {
cfCtx.close();
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
cfCtx.close();
callback.onFailure(t);
}
}, (id, cb) -> deleteCfForEntity(id, cfId, cb));
} }
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {

16
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -34,6 +34,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.asset.AssetSearchQuery;
@ -76,6 +79,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -83,6 +88,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -137,10 +143,16 @@ public class AssetController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/asset", method = RequestMethod.POST) @RequestMapping(value = "/asset", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset) throws Exception { public Asset saveAsset(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the asset.") @RequestBody Asset asset,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
asset.setTenantId(getTenantId()); asset.setTenantId(getTenantId());
checkEntity(asset.getId(), asset, Resource.ASSET); checkEntity(asset.getId(), asset, Resource.ASSET);
return tbAssetService.save(asset, getCurrentUser()); return tbAssetService.save(asset, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete asset (deleteAsset)", @ApiOperation(value = "Delete asset (deleteAsset)",

14
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -1744,4 +1744,18 @@ public class ControllerConstants {
MARKDOWN_CODE_BLOCK_END ; MARKDOWN_CODE_BLOCK_END ;
protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities)."; protected static final String SECURITY_WRITE_CHECK = " Security check is performed to verify that the user has 'WRITE' permission for the entity (entities).";
public static final String NAME_CONFLICT_POLICY_DESC = "Optional value of name conflict policy. Possible values: FAIL or UNIQUIFY. " +
" If omitted, FAIL policy is applied. FAIL policy implies exception will be thrown if an entity with the same name already exists. " +
" UNIQUIFY policy appends a suffix to the entity name, if a name conflict occurs.";
public static final String UNIQUIFY_SEPARATOR_DESC = "Optional value of name suffix separator used by UNIQUIFY policy. By default, underscore separator is used. " +
"For example, strategy is UNIQUIFY, separator is '-'; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-7fsh4f'.";
public static final String UNIQUIFY_STRATEGY_DESC = "Optional value of uniquify strategy used by UNIQUIFY policy. Possible values: RANDOM or INCREMENTAL. " +
"By default, RANDOM strategy is used, which means random alphanumeric string will be added as a suffix to entity name. " +
"INCREMENTAL implies the first possible number starting from 1 will be added as a name suffix. " +
"For example, strategy is UNIQUIFY, uniquify strategy is INCREMENTAL; if a name conflict occurs for entity name 'test-name', " +
"created entity will have name like 'test-name-1.";
} }

16
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -32,6 +32,9 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -47,6 +50,8 @@ import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -54,6 +59,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController @RestController
@ -128,10 +134,16 @@ public class CustomerController extends BaseController {
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer", method = RequestMethod.POST) @RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer) throws Exception { public Customer saveCustomer(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the customer.") @RequestBody Customer customer,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
customer.setTenantId(getTenantId()); customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER); checkEntity(customer.getId(), customer, Resource.CUSTOMER);
return tbCustomerService.save(customer, getCurrentUser()); return tbCustomerService.save(customer, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete Customer (deleteCustomer)", @ApiOperation(value = "Delete Customer (deleteCustomer)",

27
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -46,8 +46,11 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -108,6 +111,8 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -117,6 +122,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
@ -177,14 +183,21 @@ public class DeviceController extends BaseController {
@ResponseBody @ResponseBody
public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device, public Device saveDevice(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the device.") @RequestBody Device device,
@Parameter(description = "Optional value of the device credentials to be used during device creation. " + @Parameter(description = "Optional value of the device credentials to be used during device creation. " +
"If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws Exception { "If omitted, access token will be auto-generated.")
@RequestParam(name = "accessToken", required = false) String accessToken,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
if (device.getId() != null) { if (device.getId() != null) {
checkDeviceId(device.getId(), Operation.WRITE); checkDeviceId(device.getId(), Operation.WRITE);
} else { } else {
checkEntity(null, device, Resource.DEVICE); checkEntity(null, device, Resource.DEVICE);
} }
return tbDeviceService.save(device, accessToken, getCurrentUser()); return tbDeviceService.save(device, accessToken, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Create Device (saveDevice) with credentials ", @ApiOperation(value = "Create Device (saveDevice) with credentials ",
@ -209,12 +222,18 @@ public class DeviceController extends BaseController {
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.") public Device saveDeviceWithCredentials(@Parameter(description = "The JSON object with device and credentials. See method description above for example.")
@Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { @Valid @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws ThingsboardException {
Device device = deviceAndCredentials.getDevice(); Device device = deviceAndCredentials.getDevice();
DeviceCredentials credentials = deviceAndCredentials.getCredentials(); DeviceCredentials credentials = deviceAndCredentials.getCredentials();
device.setTenantId(getCurrentUser().getTenantId()); device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE); checkEntity(device.getId(), device, Resource.DEVICE);
return tbDeviceService.saveDeviceWithCredentials(device, credentials, getCurrentUser()); return tbDeviceService.saveDeviceWithCredentials(device, credentials, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete device (deleteDevice)", @ApiOperation(value = "Delete device (deleteDevice)",

16
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.UniquifyStrategy;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -69,6 +72,8 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NAME_CONFLICT_POLICY_DESC;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_SEPARATOR_DESC;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
@ -76,6 +81,7 @@ import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_D
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UNIQUIFY_STRATEGY_DESC;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/** /**
@ -128,7 +134,13 @@ public class EntityViewController extends BaseController {
@ResponseBody @ResponseBody
public EntityView saveEntityView( public EntityView saveEntityView(
@Parameter(description = "A JSON object representing the entity view.") @Parameter(description = "A JSON object representing the entity view.")
@RequestBody EntityView entityView) throws Exception { @RequestBody EntityView entityView,
@Parameter(description = NAME_CONFLICT_POLICY_DESC)
@RequestParam(name = "nameConflictPolicy", defaultValue = "FAIL") NameConflictPolicy nameConflictPolicy,
@Parameter(description = UNIQUIFY_SEPARATOR_DESC)
@RequestParam(name = "uniquifySeparator", defaultValue = "_") String uniquifySeparator,
@Parameter(description = UNIQUIFY_STRATEGY_DESC)
@RequestParam(name = "uniquifyStrategy", defaultValue = "RANDOM") UniquifyStrategy uniquifyStrategy) throws Exception {
entityView.setTenantId(getCurrentUser().getTenantId()); entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null; EntityView existingEntityView = null;
if (entityView.getId() == null) { if (entityView.getId() == null) {
@ -137,7 +149,7 @@ public class EntityViewController extends BaseController {
} else { } else {
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE); existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
} }
return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser()); return tbEntityViewService.save(entityView, existingEntityView, new NameConflictStrategy(nameConflictPolicy, uniquifySeparator, uniquifyStrategy), getCurrentUser());
} }
@ApiOperation(value = "Delete entity view (deleteEntityView)", @ApiOperation(value = "Delete entity view (deleteEntityView)",

5
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -27,6 +27,8 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MServerSecurityConfigDefault;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -37,6 +39,7 @@ import org.thingsboard.server.service.lwm2m.LwM2MService;
import java.util.Map; import java.util.Map;
import static org.thingsboard.server.common.data.NameConflictStrategy.DEFAULT;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@ -73,6 +76,6 @@ public class Lwm2mController extends BaseController {
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException { public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); Device device = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); DeviceCredentials credentials = checkNotNull(JacksonUtil.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials), DEFAULT.policy(), DEFAULT.separator(), DEFAULT.uniquifyStrategy());
} }
} }

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

@ -58,6 +58,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.io.Closeable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -66,11 +67,10 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Data @Data
@Slf4j @Slf4j
public class CalculatedFieldCtx { public class CalculatedFieldCtx implements Closeable {
private CalculatedField calculatedField; private CalculatedField calculatedField;
@ -197,11 +197,8 @@ public class CalculatedFieldCtx {
} }
case ALARM -> { case ALARM -> {
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
Stream<AlarmRule> rules = configuration.getCreateRules().values().stream(); configuration.getAllRules().map(rule -> rule.getValue().getCondition().getExpression())
if (configuration.getClearRule() != null) { .forEach(expression -> {
rules = Stream.concat(rules, Stream.of(configuration.getClearRule()));
}
rules.map(rule -> rule.getCondition().getExpression()).forEach(expression -> {
if (expression instanceof TbelAlarmConditionExpression tbelExpression) { if (expression instanceof TbelAlarmConditionExpression tbelExpression) {
initTbelExpression(tbelExpression.getExpression()); initTbelExpression(tbelExpression.getExpression());
} }
@ -259,7 +256,6 @@ public class CalculatedFieldCtx {
public ScheduledFuture<?> scheduleReevaluation(long delayMs, TbActorRef actorCtx) { public ScheduledFuture<?> scheduleReevaluation(long delayMs, TbActorRef actorCtx) {
log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs); log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs);
// TODO: use single lazy-loaded instance of CalculatedFieldReevaluateMsg
return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs); return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs);
} }
@ -508,9 +504,18 @@ public class CalculatedFieldCtx {
if (!Objects.equals(output, other.output)) { if (!Objects.equals(output, other.output)) {
return true; return true;
} }
if (cfType == CalculatedFieldType.ALARM && !calculatedField.getName().equals(other.getCalculatedField().getName())) { if (cfType == CalculatedFieldType.ALARM) {
if (!calculatedField.getName().equals(other.getCalculatedField().getName())) {
return true;
}
var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration();
if (!thisConfig.rulesEqual(otherConfig, AlarmRule::equals)) {
// if the rules have any changes not tracked by hasStateChanges
return true; return true;
} }
}
return scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis; return scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis;
} }
@ -521,8 +526,10 @@ public class CalculatedFieldCtx {
if (cfType == CalculatedFieldType.ALARM) { if (cfType == CalculatedFieldType.ALARM) {
var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration();
if (!thisConfig.getCreateRules().equals(otherConfig.getCreateRules()) || if (!thisConfig.rulesEqual(otherConfig, (thisRule, otherRule) -> {
!Objects.equals(thisConfig.getClearRule(), otherConfig.getClearRule())) { return thisRule.getCondition().getType() == otherRule.getCondition().getType();
})) {
// reinitializing only if the rule list changed, or if a condition type changed for any rule
return true; return true;
} }
} }
@ -562,13 +569,18 @@ public class CalculatedFieldCtx {
}; };
} }
public void stop() { @Override
public void close() {
try {
if (tbelExpressions != null) { if (tbelExpressions != null) {
tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy);
} }
if (simpleExpressions != null) { if (simpleExpressions != null) {
simpleExpressions.values().forEach(ThreadLocal::remove); simpleExpressions.values().forEach(ThreadLocal::remove);
} }
} catch (Exception e) {
log.warn("Failed to stop {}", this, e);
}
} }
public String getSizeExceedsLimitMessage() { public String getSizeExceedsLimitMessage() {

20
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -103,6 +103,18 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
this.configuration = getConfiguration(ctx); this.configuration = getConfiguration(ctx);
this.alarmType = ctx.getCalculatedField().getName(); this.alarmType = ctx.getCalculatedField().getName();
Map<AlarmSeverity, AlarmRule> createRules = configuration.getCreateRules();
createRules.forEach((severity, rule) -> {
AlarmRuleState ruleState = createRuleStates.get(severity);
if (ruleState != null) {
ruleState.setAlarmRule(rule);
}
});
AlarmRule clearRule = configuration.getClearRule();
if (clearRule != null && clearRuleState != null) {
clearRuleState.setAlarmRule(clearRule);
}
if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) { if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) {
currentAlarm = null; currentAlarm = null;
initialFetchDone = false; initialFetchDone = false;
@ -189,8 +201,6 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
@Override @Override
public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) { public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) {
initCurrentAlarm(ctx); initCurrentAlarm(ctx);
// FIXME: don't create alarm if attrs were deleted, or config is updated
// TODO: what if expression is changed? do we reevaluate? or only on new events?
TbAlarmResult result = createOrClearAlarms(state -> { TbAlarmResult result = createOrClearAlarms(state -> {
if (updatedArgs != null) { if (updatedArgs != null) {
boolean newEvent = !updatedArgs.isEmpty(); boolean newEvent = !updatedArgs.isEmpty();
@ -222,10 +232,8 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private void processAlarmClear(Alarm alarm) { private void processAlarmClear(Alarm alarm) {
currentAlarm = null; currentAlarm = null;
createRuleStates.values().forEach(AlarmRuleState::clear); createRuleStates.values().forEach(this::clearState);
createRuleStates.clear();
clearState(clearRuleState); clearState(clearRuleState);
clearRuleState = null;
} }
private void processAlarmAck(Alarm alarm) { private void processAlarmAck(Alarm alarm) {
@ -269,7 +277,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
clearState(state); clearState(state);
} }
AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm( AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm(
ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), true ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), false
); );
if (clearResult.isCleared()) { if (clearResult.isCleared()) {
result = TbAlarmResult.builder() result = TbAlarmResult.builder()

22
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -237,7 +237,15 @@ public class AlarmRuleState {
} }
public void clear() { public void clear() {
clearRepeatingConditionState();
clearDurationConditionState();
}
private void clearRepeatingConditionState() {
eventCount = 0L; eventCount = 0L;
}
private void clearDurationConditionState() {
firstEventTs = 0L; firstEventTs = 0L;
lastEventTs = 0L; lastEventTs = 0L;
duration = 0L; duration = 0L;
@ -289,6 +297,20 @@ public class AlarmRuleState {
public void setAlarmRule(AlarmRule alarmRule) { public void setAlarmRule(AlarmRule alarmRule) {
this.alarmRule = alarmRule; this.alarmRule = alarmRule;
this.condition = alarmRule.getCondition(); this.condition = alarmRule.getCondition();
// clearing state for other condition types (possibly left from a previous condition type)
switch (condition.getType()) {
case SIMPLE -> {
clearRepeatingConditionState();
clearDurationConditionState();
}
case REPEATING -> {
clearDurationConditionState();
}
case DURATION -> {
clearRepeatingConditionState();
}
}
} }
public StateInfo getStateInfo() { public StateInfo getStateInfo() {

7
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -24,6 +24,7 @@ import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
@ -59,6 +60,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings; import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService; import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.ai.AiModelProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
@ -261,6 +263,11 @@ public class EdgeContextComponent {
@Autowired @Autowired
private CalculatedFieldProcessor calculatedFieldProcessor; private CalculatedFieldProcessor calculatedFieldProcessor;
@Autowired
private AiModelService aiModelService;
@Autowired
private AiModelProcessor aiModelProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType); EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) { if (processor == null) {

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java

@ -113,7 +113,7 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { try {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) { if (EntityType.TENANT == entityType || EntityType.EDGE == entityType) {
return; return;
} }
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -227,7 +227,7 @@ public class EdgeEventSourcingListener {
break; break;
case TENANT: case TENANT:
return !event.getCreated(); return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL: case API_USAGE_STATE, EDGE:
return false; return false;
case DOMAIN: case DOMAIN:
if (entity instanceof Domain domain) { if (entity instanceof Domain domain) {

16
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -86,6 +88,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -654,4 +657,17 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build(); .setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
} }
public static AiModelUpdateMsg constructAiModelUpdatedMsg(UpdateMsgType msgType, AiModel aiModel) {
return AiModelUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(aiModel))
.setIdMSB(aiModel.getId().getId().getMostSignificantBits())
.setIdLSB(aiModel.getId().getId().getLeastSignificantBits()).build();
}
public static AiModelUpdateMsg constructAiModelDeleteMsg(AiModelId aiModelId) {
return AiModelUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(aiModelId.getId().getMostSignificantBits())
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
} }

6
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; import org.thingsboard.server.dao.edge.stats.EdgeStatsKey;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -934,6 +935,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
} }
} }
if (uplinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : uplinkMsg.getAiModelUpdateMsgList()) {
result.add(ctx.getAiModelProcessor().processAiModelMsgFromEdge(edge.getTenantId(), edge, aiModelUpdateMsg));
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -139,7 +139,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true; UPDATED_COMMENT, DELETED -> true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE, WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, AI_MODEL, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true; NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false; default -> false;
}; };

130
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java

@ -0,0 +1,130 @@
/**
* 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.service.edge.rpc.processor.ai;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiModelProcessor {
@Override
public ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg) {
AiModelId aiModelId = new AiModelId(new UUID(aiModelUpdateMsg.getIdMSB(), aiModelUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (aiModelUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
AiModelId aiModelId = new AiModelId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(edgeEvent.getTenantId(), aiModelId);
if (aiModel.isPresent()) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelUpdatedMsg(msgType, aiModel.get());
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
case DELETED -> {
AiModelUpdateMsg aiModelUpdateMsg = EdgeMsgConstructorUtils.constructAiModelDeleteMsg(aiModelId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAiModelUpdateMsg(aiModelUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.AI_MODEL;
}
private void processAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateAiModel(tenantId, aiModelId, aiModelUpdateMsg);
Boolean wasCreated = resultPair.getFirst();
if (wasCreated) {
pushAiModelCreatedEventToRuleEngine(tenantId, edge, aiModelId);
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL, EdgeEventActionType.UPDATED, aiModelId, null);
}
}
private void pushAiModelCreatedEventToRuleEngine(TenantId tenantId, Edge edge, AiModelId aiModelId) {
try {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModel.isPresent()) {
String aiModelAsString = JacksonUtil.toString(aiModel.get());
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
pushEntityEventToRuleEngine(tenantId, aiModelId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, aiModelAsString, msgMetaData);
} else {
log.warn("[{}][{}] Failed to find aiModel", tenantId, aiModelId);
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to push aiModel action to rule engine: {}", tenantId, aiModelId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelProcessor.java

@ -0,0 +1,28 @@
/**
* 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.service.edge.rpc.processor.ai;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface AiModelProcessor extends EdgeProcessor {
ListenableFuture<Void> processAiModelMsgFromEdge(TenantId tenantId, Edge edge, AiModelUpdateMsg aiModelUpdateMsg);
}

81
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java

@ -0,0 +1,81 @@
/**
* 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.service.edge.rpc.processor.ai;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
import java.util.Optional;
@Slf4j
public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<AiModel> aiModelValidator;
protected Pair<Boolean, Boolean> saveOrUpdateAiModel(TenantId tenantId, AiModelId aiModelId, AiModelUpdateMsg aiModelUpdateMsg) {
boolean isCreated = false;
boolean isNameUpdated = false;
try {
AiModel aiModel = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
if (aiModel == null) {
throw new RuntimeException("[{" + tenantId + "}] aiModelUpdateMsg {" + aiModelUpdateMsg + " } cannot be converted to aiModel");
}
Optional<AiModel> aiModelById = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModelById.isEmpty()) {
aiModel.setCreatedTime(Uuids.unixTimestamp(aiModelId.getId()));
isCreated = true;
aiModel.setId(null);
} else {
aiModel.setId(aiModelId);
}
String aiModelName = aiModel.getName();
Optional<AiModel> aiModelByName = edgeCtx.getAiModelService().findAiModelByTenantIdAndName(aiModel.getTenantId(), aiModelName);
if (aiModelByName.isPresent() && !aiModelByName.get().getId().equals(aiModelId)) {
aiModelName = aiModelName + "_" + StringUtils.randomAlphabetic(15);
log.warn("[{}] aiModel with name {} already exists. Renaming aiModel name to {}",
tenantId, aiModel.getName(), aiModelByName.get().getName());
isNameUpdated = true;
}
aiModel.setName(aiModelName);
aiModelValidator.validate(aiModel, AiModel::getTenantId);
if (isCreated) {
aiModel.setId(aiModelId);
}
edgeCtx.getAiModelService().save(aiModel, false);
} catch (Exception e) {
log.error("[{}] Failed to process aiModel update msg [{}]", tenantId, aiModelUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, isNameUpdated);
}
}

1
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -255,7 +255,6 @@ public class EntityStateSourcingListener {
if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) { if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) {
ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder() ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder()
.setEventMsg(toProto(event)) .setEventMsg(toProto(event))
.addCfTypes(CalculatedFieldType.ALARM.name())
.build(); .build();
tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() { tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() {
@Override @Override

8
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -20,6 +20,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -40,10 +41,15 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
@Override @Override
public Asset save(Asset asset, User user) throws Exception { public Asset save(Asset asset, User user) throws Exception {
return save(asset, NameConflictStrategy.DEFAULT, user);
}
@Override
public Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = asset.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = asset.getTenantId(); TenantId tenantId = asset.getTenantId();
try { try {
Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); Asset savedAsset = checkNotNull(assetService.saveAsset(asset, nameConflictStrategy));
autoCommit(user, savedAsset.getId()); autoCommit(user, savedAsset.getId());
logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedAsset.getId(), savedAsset, asset.getCustomerId(),
actionType, user); actionType, user);

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.asset; package org.thingsboard.server.service.entitiy.asset;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -27,6 +28,8 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception; Asset save(Asset asset, User user) throws Exception;
Asset save(Asset asset, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void delete(Asset asset, User user); void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException; Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -32,10 +33,15 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
@Override @Override
public Customer save(Customer customer, SecurityUser user) throws Exception { public Customer save(Customer customer, SecurityUser user) throws Exception {
return save(customer, NameConflictStrategy.DEFAULT, user);
}
@Override
public Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception {
ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = customer.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = customer.getTenantId(); TenantId tenantId = customer.getTenantId();
try { try {
Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer)); Customer savedCustomer = checkNotNull(customerService.saveCustomer(customer, nameConflictStrategy));
autoCommit(user, savedCustomer.getId()); autoCommit(user, savedCustomer.getId());
logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user); logEntityActionService.logEntityAction(tenantId, savedCustomer.getId(), savedCustomer, null, actionType, user);
return savedCustomer; return savedCustomer;

4
application/src/main/java/org/thingsboard/server/service/entitiy/customer/TbCustomerService.java

@ -16,8 +16,12 @@
package org.thingsboard.server.service.entitiy.customer; package org.thingsboard.server.service.entitiy.customer;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbCustomerService extends SimpleTbEntityService<Customer> { public interface TbCustomerService extends SimpleTbEntityService<Customer> {
Customer save(Customer customer, NameConflictStrategy nameConflictStrategy, SecurityUser user) throws Exception;
} }

15
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -25,6 +25,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
@ -56,10 +57,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override @Override
public Device save(Device device, String accessToken, User user) throws Exception { public Device save(Device device, String accessToken, User user) throws Exception {
return save(device, accessToken, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId(); TenantId tenantId = device.getTenantId();
try { try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken, nameConflictStrategy));
autoCommit(user, savedDevice.getId()); autoCommit(user, savedDevice.getId());
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user); actionType, user);
@ -73,10 +79,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
@Override @Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException { public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, User user) throws ThingsboardException {
return saveDeviceWithCredentials(device, credentials, NameConflictStrategy.DEFAULT, user);
}
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials credentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException {
ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = device.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = device.getTenantId(); TenantId tenantId = device.getTenantId();
try { try {
Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials)); Device savedDevice = checkNotNull(deviceService.saveDeviceWithCredentials(device, credentials, nameConflictStrategy));
logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), logEntityActionService.logEntityAction(tenantId, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(),
actionType, user); actionType, user);

5
application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.device;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -33,8 +34,12 @@ public interface TbDeviceService {
Device save(Device device, String accessToken, User user) throws Exception; Device save(Device device, String accessToken, User user) throws Exception;
Device save(Device device, String accessToken, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException; Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy, User user) throws ThingsboardException;
void delete(Device device, User user); void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException; Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;

8
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -80,10 +81,15 @@ public class DefaultTbEntityViewService extends AbstractTbEntityService implemen
@Override @Override
public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception { public EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception {
return save(entityView, existingEntityView, NameConflictStrategy.DEFAULT, user);
}
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED; ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = entityView.getTenantId(); TenantId tenantId = entityView.getTenantId();
try { try {
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView)); EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView, nameConflictStrategy));
this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user); this.updateEntityViewAttributes(tenantId, savedEntityView, existingEntityView, user);
autoCommit(user, savedEntityView.getId()); autoCommit(user, savedEntityView.getId());
logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView, logEntityActionService.logEntityAction(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,

3
application/src/main/java/org/thingsboard/server/service/entitiy/entityview/TbEntityViewService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.entitiy.entityview;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -33,6 +34,8 @@ public interface TbEntityViewService extends ComponentLifecycleListener {
EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception; EntityView save(EntityView entityView, EntityView existingEntityView, User user) throws Exception;
EntityView save(EntityView entityView, EntityView existingEntityView, NameConflictStrategy nameConflictStrategy, User user) throws Exception;
void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException; void updateEntityViewAttributes(TenantId tenantId, EntityView savedEntityView, EntityView oldEntityView, User user) throws ThingsboardException;
void delete(EntityView entity, User user) throws ThingsboardException; void delete(EntityView entity, User user) throws ThingsboardException;

7
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -82,6 +82,7 @@ import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.mobile.app.MobileApp; import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.queue.ProcessingStrategy; import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.ProcessingStrategyType; import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.Queue;
@ -456,6 +457,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter temperatureAlarmFlagFilter = new AlarmConditionFilter(); AlarmConditionFilter temperatureAlarmFlagFilter = new AlarmConditionFilter();
temperatureAlarmFlagFilter.setArgument("temperatureAlarmFlag"); temperatureAlarmFlagFilter.setArgument("temperatureAlarmFlag");
temperatureAlarmFlagFilter.setValueType(EntityKeyValueType.BOOLEAN);
BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate(); BooleanFilterPredicate temperatureAlarmFlagAttributePredicate = new BooleanFilterPredicate();
temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); temperatureAlarmFlagAttributePredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
temperatureAlarmFlagAttributePredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); temperatureAlarmFlagAttributePredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null));
@ -463,6 +465,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); AlarmConditionFilter temperatureFilter = new AlarmConditionFilter();
temperatureFilter.setArgument("temperature"); temperatureFilter.setArgument("temperature");
temperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate temperatureFilterPredicate = new NumericFilterPredicate(); NumericFilterPredicate temperatureFilterPredicate = new NumericFilterPredicate();
temperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); temperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
temperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); temperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold"));
@ -479,6 +482,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter clearTemperatureFilter = new AlarmConditionFilter(); AlarmConditionFilter clearTemperatureFilter = new AlarmConditionFilter();
clearTemperatureFilter.setArgument("temperature"); clearTemperatureFilter.setArgument("temperature");
clearTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate clearTemperatureFilterPredicate = new NumericFilterPredicate(); NumericFilterPredicate clearTemperatureFilterPredicate = new NumericFilterPredicate();
clearTemperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL); clearTemperatureFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS_OR_EQUAL);
clearTemperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold")); clearTemperatureFilterPredicate.setValue(new AlarmConditionValue<>(null, "temperatureAlarmThreshold"));
@ -517,6 +521,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter(); AlarmConditionFilter humidityAlarmFlagAttributeFilter = new AlarmConditionFilter();
humidityAlarmFlagAttributeFilter.setArgument("humidityAlarmFlag"); humidityAlarmFlagAttributeFilter.setArgument("humidityAlarmFlag");
humidityAlarmFlagAttributeFilter.setValueType(EntityKeyValueType.BOOLEAN);
BooleanFilterPredicate humidityAlarmFlagPredicate = new BooleanFilterPredicate(); BooleanFilterPredicate humidityAlarmFlagPredicate = new BooleanFilterPredicate();
humidityAlarmFlagPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); humidityAlarmFlagPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL);
humidityAlarmFlagPredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null)); humidityAlarmFlagPredicate.setValue(new AlarmConditionValue<>(Boolean.TRUE, null));
@ -524,6 +529,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter humidityFilter = new AlarmConditionFilter(); AlarmConditionFilter humidityFilter = new AlarmConditionFilter();
humidityFilter.setArgument("humidity"); humidityFilter.setArgument("humidity");
humidityFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate humidityFilterPredicate = new NumericFilterPredicate(); NumericFilterPredicate humidityFilterPredicate = new NumericFilterPredicate();
humidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); humidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS);
humidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); humidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold"));
@ -540,6 +546,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
AlarmConditionFilter clearHumidityFilter = new AlarmConditionFilter(); AlarmConditionFilter clearHumidityFilter = new AlarmConditionFilter();
clearHumidityFilter.setArgument("humidity"); clearHumidityFilter.setArgument("humidity");
clearHumidityFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate clearHumidityFilterPredicate = new NumericFilterPredicate(); NumericFilterPredicate clearHumidityFilterPredicate = new NumericFilterPredicate();
clearHumidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL); clearHumidityFilterPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER_OR_EQUAL);
clearHumidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold")); clearHumidityFilterPredicate.setValue(new AlarmConditionValue<>(null, "humidityAlarmThreshold"));

18
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.queue; package org.thingsboard.server.service.queue;
import com.google.protobuf.ProtocolStringList;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -28,7 +27,6 @@ import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTeleme
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -61,7 +59,6 @@ import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedCon
import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -183,8 +180,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
} }
private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) { private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) {
Set<CalculatedFieldType> cfTypes = getCfTypes(toCfMsg.getCfTypesList()); if (toCfMsg.hasTelemetryMsg()) {
if (toCfMsg.hasTelemetryMsg()) { // TODO: add CF type filter to the message. or just rename the CF strategy to "Process alarms and calculated fields
log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg());
forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback);
} else if (toCfMsg.hasLinkedTelemetryMsg()) { } else if (toCfMsg.hasLinkedTelemetryMsg()) {
@ -264,18 +260,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB));
} }
private Set<CalculatedFieldType> getCfTypes(ProtocolStringList cfTypesList) {
Set<CalculatedFieldType> cfTypes;
if (cfTypesList.isEmpty()) {
cfTypes = EnumSet.allOf(CalculatedFieldType.class);
} else {
cfTypes = cfTypesList.stream()
.map(CalculatedFieldType::valueOf)
.collect(Collectors.toSet());
}
return cfTypes;
}
@Override @Override
protected void stopConsumers() { protected void stopConsumers() {
super.stopConsumers(); super.stopConsumers();

9
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.sync.ie.importing.csv;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
@ -183,7 +184,13 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
data.entrySet().stream() data.entrySet().stream()
.filter(dataEntry -> dataEntry.getKey().getType() == kvType && .filter(dataEntry -> dataEntry.getKey().getType() == kvType &&
StringUtils.isNotEmpty(dataEntry.getKey().getKey())) StringUtils.isNotEmpty(dataEntry.getKey().getKey()))
.forEach(dataEntry -> kvs.add(dataEntry.getKey().getKey(), dataEntry.getValue().toJsonPrimitive())); .forEach(dataEntry -> {
ParsedValue value = dataEntry.getValue();
JsonElement kvValue = (value.getDataType() == DataType.JSON)
? (JsonElement) value.getValue()
: value.toJsonPrimitive();
kvs.add(dataEntry.getKey().getKey(), kvValue);
});
return Map.entry(kvType, kvs); return Map.entry(kvType, kvs);
}) })
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) .filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0)

19
application/src/main/java/org/thingsboard/server/utils/CsvUtils.java

@ -17,10 +17,15 @@ package org.thingsboard.server.utils;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.input.CharSequenceReader; import org.apache.commons.io.input.CharSequenceReader;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -43,4 +48,18 @@ public class CsvUtils {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@SneakyThrows
public static byte[] generateCsv(List<List<String>> rows) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT)) {
for (List<String> row : rows) {
csvPrinter.printRecord(row);
}
csvPrinter.flush();
}
return out.toByteArray();
}
} }

107
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule; import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.id.EventId;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.event.EventDao; import org.thingsboard.server.dao.event.EventDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -166,6 +168,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter filter = new AlarmConditionFilter(); AlarmConditionFilter filter = new AlarmConditionFilter();
filter.setArgument("temperature"); filter.setArgument("temperature");
filter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate predicate = new NumericFilterPredicate(); NumericFilterPredicate predicate = new NumericFilterPredicate();
predicate.setOperation(NumericOperation.GREATER_OR_EQUAL); predicate.setOperation(NumericOperation.GREATER_OR_EQUAL);
AlarmConditionValue<Double> thresholdValue = new AlarmConditionValue<>(); AlarmConditionValue<Double> thresholdValue = new AlarmConditionValue<>();
@ -457,7 +460,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); schedule = schedule.replace("\"enabled\":false", "\"enabled\":true");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}");
checkAlarmResult(calculatedField, alarmResult -> {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
// checking multiple debug events due to scheduled reevaluation (which also produces debug events)
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -637,11 +647,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null)
); );
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm",
arguments, createRules, null); arguments, createRules, null, configuration -> {
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(""" "temperature is ${temperature}, humidity is ${humidity}"
temperature is ${temperature}, humidity is ${humidity}"""); );
calculatedField = saveCalculatedField(calculatedField); });
postTelemetry(deviceId, "{\"temperature\":50}"); postTelemetry(deviceId, "{\"temperature\":50}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}");
@ -652,6 +662,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("temperature is 50, humidity is 50"); .isEqualTo("temperature is 50, humidity is 50");
}); });
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"UPDATED temperature is ${temperature}, humidity is ${humidity}"
);
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCreated()).isFalse();
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("UPDATED temperature is 50, humidity is 50");
});
} }
@Test @Test
@ -682,7 +704,46 @@ public class AlarmRulesTest extends AbstractControllerTest {
Thread.sleep(10000); Thread.sleep(10000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
}
@Test
public void testManualClearAlarm() throws Exception {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
temperatureArgument.setDefaultValue("0");
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
}).getAlarm();
doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class);
Thread.sleep(1000);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> { checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId());
assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -690,13 +751,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
} }
// TODO: MSA tests // TODO: MSA tests
// TODO: test when attribute or telemetry is deleted without default value - perform calculation not happens
private void checkAlarmResult(CalculatedField calculatedField, Consumer<TbAlarmResult> assertion) { private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer<TbAlarmResult> assertion) {
checkAlarmResult(calculatedField, null, assertion); return checkAlarmResult(calculatedField, null, assertion);
} }
private void checkAlarmResult(CalculatedField calculatedField, private TbAlarmResult checkAlarmResult(CalculatedField calculatedField,
Predicate<TbAlarmResult> waitFor, Predicate<TbAlarmResult> waitFor,
Consumer<TbAlarmResult> assertion) { Consumer<TbAlarmResult> assertion) {
TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS) TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS)
@ -707,6 +767,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
Alarm alarm = alarmResult.getAlarm(); Alarm alarm = alarmResult.getAlarm();
assertThat(alarm.getOriginator()).isEqualTo(originatorId); assertThat(alarm.getOriginator()).isEqualTo(originatorId);
assertThat(alarm.getType()).isEqualTo(calculatedField.getName()); assertThat(alarm.getType()).isEqualTo(calculatedField.getName());
return alarmResult;
} }
private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) { private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) {
@ -729,24 +790,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
String alarmType, String alarmType,
Map<String, Argument> arguments, Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions, Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition) { Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>(); Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>();
createConditions.forEach((severity, condition) -> { createConditions.forEach((severity, condition) -> {
createRules.put(severity, toAlarmRule(condition)); createRules.put(severity, toAlarmRule(condition));
}); });
AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null;
CalculatedField calculatedField = createAlarmCf(entityId, alarmType, arguments, createRules, clearRule);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getDebugEvents(calculatedField.getId(), 1), events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return calculatedField;
}
private CalculatedField createAlarmCf(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, AlarmRule> createRules,
AlarmRule clearRule) {
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId); calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType); calculatedField.setName(alarmType);
@ -757,7 +808,16 @@ public class AlarmRulesTest extends AbstractControllerTest {
configuration.setClearRule(clearRule); configuration.setClearRule(clearRule);
calculatedField.setConfiguration(configuration); calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all()); calculatedField.setDebugSettings(DebugSettings.all());
return saveCalculatedField(calculatedField); if (modifier.length > 0) {
modifier[0].accept(configuration);
}
CalculatedField savedCalculatedField = saveCalculatedField(calculatedField);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDebugEvents(savedCalculatedField.getId(), 1),
events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return savedCalculatedField;
} }
private AlarmRule toAlarmRule(Condition condition) { private AlarmRule toAlarmRule(Condition condition) {
@ -796,6 +856,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression(); SimpleAlarmConditionExpression simpleExpression = new SimpleAlarmConditionExpression();
AlarmConditionFilter filter = new AlarmConditionFilter(); AlarmConditionFilter filter = new AlarmConditionFilter();
filter.setArgument(argument); filter.setArgument(argument);
filter.setValueType(EntityKeyValueType.STRING);
StringFilterPredicate predicate = new StringFilterPredicate(); StringFilterPredicate predicate = new StringFilterPredicate();
predicate.setOperation(stringOperation); predicate.setOperation(stringOperation);
predicate.setValue(conditionValue); predicate.setValue(conditionValue);

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

@ -1025,8 +1025,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cf.setConfigurationVersion(1); cf.setConfigurationVersion(1);
PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration();
cfg.setDirection(EntitySearchDirection.TO); cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
cfg.setApplyExpressionToResolvedArguments(true); cfg.setApplyExpressionToResolvedArguments(true);
Argument arg = new Argument(); Argument arg = new Argument();
@ -1105,8 +1104,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cf.setConfigurationVersion(1); cf.setConfigurationVersion(1);
PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration(); PropagationCalculatedFieldConfiguration cfg = new PropagationCalculatedFieldConfiguration();
cfg.setDirection(EntitySearchDirection.TO); cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode
Argument arg = new Argument(); Argument arg = new Argument();

24
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -1080,6 +1080,30 @@ public class AssetControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId); testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId);
} }
@Test
public void testSaveAssetWithUniquifyStrategy() throws Exception {
Asset asset = new Asset();
asset.setName("My unique asset");
asset.setType("default");
doPost("/api/asset", asset, Asset.class);
doPost("/api/asset", asset).andExpect(status().isBadRequest());
doPost("/api/asset?nameConflictPolicy=FAIL", asset).andExpect(status().isBadRequest());
Asset secondAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY", asset, Asset.class);
assertThat(secondAsset.getName()).startsWith("My unique asset_");
Asset thirdAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", asset, Asset.class);
assertThat(thirdAsset.getName()).startsWith("My unique asset-");
Asset fourthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fourthAsset.getName()).isEqualTo("My unique asset_1");
Asset fifthAsset = doPost("/api/asset?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", asset, Asset.class);
assertThat(fifthAsset.getName()).isEqualTo("My unique asset_2");
}
private Asset createAsset(String name) { private Asset createAsset(String name) {
Asset asset = new Asset(); Asset asset = new Asset();
asset.setName(name); asset.setName(name);

3
application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java

@ -271,8 +271,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig(Map<String, Argument> arguments) { private CalculatedFieldConfiguration getPropagationCalculatedFieldConfig(Map<String, Argument> arguments) {
var config = new PropagationCalculatedFieldConfiguration(); var config = new PropagationCalculatedFieldConfiguration();
config.setRelationType(EntityRelation.CONTAINS_TYPE); config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
config.setDirection(EntitySearchDirection.TO);
config.setApplyExpressionToResolvedArguments(false); config.setApplyExpressionToResolvedArguments(false);
config.setExpression(null); config.setExpression(null);

22
application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java

@ -33,6 +33,7 @@ import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
@ -462,6 +463,27 @@ public class CustomerControllerTest extends AbstractControllerTest {
testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId); testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId);
} }
@Test
public void testSaveCustomerWithUniquifyStrategy() throws Exception {
Customer customer = new Customer();
customer.setTitle("My unique customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
doPost("/api/customer?nameConflictPolicy=FAIL", customer).andExpect(status().isBadRequest());
Customer secondCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY", customer, Customer.class);
assertThat(secondCustomer.getName()).startsWith("My unique customer_");
Customer thirdCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", customer, Customer.class);
assertThat(thirdCustomer.getName()).startsWith("My unique customer-");
Customer fourthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fourthCustomer.getName()).isEqualTo("My unique customer_1");
Customer fifthCustomer = doPost("/api/customer?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", customer, Customer.class);
assertThat(fifthCustomer.getName()).isEqualTo("My unique customer_2");
}
private Customer createCustomer(String title) { private Customer createCustomer(String title) {
Customer customer = new Customer(); Customer customer = new Customer();
customer.setTitle(title); customer.setTitle(title);

81
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -38,6 +38,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.shaded.org.awaitility.Awaitility; import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
@ -59,6 +60,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
@ -77,10 +79,15 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.utils.CsvUtils;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -1586,6 +1593,56 @@ public class DeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(newAttributeValue, actualAttribute.get("value")); Assert.assertEquals(newAttributeValue, actualAttribute.get("value"));
} }
@Test
public void testBulkImportDeviceWithJsonAttr() throws Exception {
String deviceName = "some_device";
String deviceType = "some_type";
String deviceAttr = "{\"threshold\":45}";
List<List<String>> content = new LinkedList<>();
content.add(Arrays.asList("NAME", "TYPE", "ATTR"));
content.add(Arrays.asList(deviceName, deviceType, deviceAttr));
byte[] bytes = CsvUtils.generateCsv(content);
BulkImportRequest request = new BulkImportRequest();
request.setFile(new String(bytes, StandardCharsets.UTF_8));
BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
name.setType(BulkImportColumnType.NAME);
BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
type.setType(BulkImportColumnType.TYPE);
BulkImportRequest.ColumnMapping attr = new BulkImportRequest.ColumnMapping();
attr.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
attr.setKey("attr");
List<BulkImportRequest.ColumnMapping> columns = new ArrayList<>();
columns.add(name);
columns.add(type);
columns.add(attr);
mapping.setColumns(columns);
mapping.setDelimiter(',');
mapping.setUpdate(true);
mapping.setHeader(true);
request.setMapping(mapping);
BulkImportResult<Device> deviceBulkImportResult = doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Assert.assertEquals(1, deviceBulkImportResult.getCreated().get());
Assert.assertEquals(0, deviceBulkImportResult.getErrors().get());
Assert.assertEquals(0, deviceBulkImportResult.getUpdated().get());
Assert.assertTrue(deviceBulkImportResult.getErrorsList().isEmpty());
Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
Assert.assertNotNull(savedDevice);
Assert.assertEquals(deviceName, savedDevice.getName());
Assert.assertEquals(deviceType, savedDevice.getType());
Optional<AttributeKvEntry> retrieved = attributesService.find(tenantId, savedDevice.getId(), AttributeScope.SERVER_SCOPE, "attr").get();
assertThat(retrieved.get().getJsonValue().get()).isEqualTo(deviceAttr);
assertThat(retrieved.get().getStrValue()).isNotPresent();
}
@Test @Test
public void testSaveDeviceWithOutdatedVersion() throws Exception { public void testSaveDeviceWithOutdatedVersion() throws Exception {
Device device = createDevice("Device v1.0"); Device device = createDevice("Device v1.0");
@ -1608,6 +1665,30 @@ public class DeviceControllerTest extends AbstractControllerTest {
assertThat(device.getVersion()).isEqualTo(3); assertThat(device.getVersion()).isEqualTo(3);
} }
@Test
public void testSaveDeviceWithUniquifyStrategy() throws Exception {
Device device = new Device();
device.setName("My unique device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
doPost("/api/device", device).andExpect(status().isBadRequest());
doPost("/api/device?nameConflictPolicy=FAIL", device).andExpect(status().isBadRequest());
Device secondDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY", device, Device.class);
assertThat(secondDevice.getName()).startsWith("My unique device_");
Device thirdDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", device, Device.class);
assertThat(thirdDevice.getName()).startsWith("My unique device-");
Device fourthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fourthDevice.getName()).isEqualTo("My unique device_1");
Device fifthDevice = doPost("/api/device?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", device, Device.class);
assertThat(fifthDevice.getName()).isEqualTo("My unique device_2");
}
private Device createDevice(String name) { private Device createDevice(String name) {
Device device = new Device(); Device device = new Device();
device.setName(name); device.setName(name);

25
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -853,4 +853,29 @@ public class EntityViewControllerTest extends AbstractControllerTest {
EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId(); EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId); testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId);
} }
@Test
public void testSaveEntityViewWithUniquifyStrategy() throws Exception {
EntityView view = new EntityView();
view.setEntityId(testDevice.getId());
view.setTenantId(tenantId);
view.setType("default");
view.setName("My unique view");
EntityView savedView = doPost("/api/entityView", view, EntityView.class);
doPost("/api/entityView?nameConflictPolicy=FAIL", view).andExpect(status().isBadRequest());
EntityView secondView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY", view, EntityView.class);
assertThat(secondView.getName()).startsWith("My unique view_");
EntityView thirdView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifySeparator=-", view, EntityView.class);
assertThat(thirdView.getName()).startsWith("My unique view-");
EntityView fourthView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fourthView.getName()).isEqualTo("My unique view_1");
EntityView fifthEntityView = doPost("/api/entityView?nameConflictPolicy=UNIQUIFY&uniquifyStrategy=INCREMENTAL", view, EntityView.class);
assertThat(fifthEntityView.getName()).isEqualTo("My unique view_2");
}
} }

196
application/src/test/java/org/thingsboard/server/edge/AiModelEdgeTest.java

@ -0,0 +1,196 @@
/**
* 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.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AiModelEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_AI_MODEL_NAME = "Edge Test AiModel";
private static final String UPDATED_AI_MODEL_NAME = "Updated Edge Test AiModel";
@Test
public void testAiModel_create_update_delete() throws Exception {
// create AiModel
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
AiModelUpdateMsg aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
AiModel aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertEquals(savedAiModel.getTenantId(), aiModelFromMsg.getTenantId());
// update AiModel
edgeImitator.expectMessageAmount(1);
savedAiModel.setName(UPDATED_AI_MODEL_NAME);
savedAiModel = doPost("/api/ai/model", savedAiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
aiModelFromMsg = JacksonUtil.fromString(aiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_AI_MODEL_NAME, aiModelFromMsg.getName());
// delete AiModel
edgeImitator.expectMessageAmount(1);
doDelete("/api/ai/model/" + savedAiModel.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof AiModelUpdateMsg);
aiModelUpdateMsg = (AiModelUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, aiModelUpdateMsg.getMsgType());
Assert.assertEquals(savedAiModel.getUuidId().getMostSignificantBits(), aiModelUpdateMsg.getIdMSB());
Assert.assertEquals(savedAiModel.getUuidId().getLeastSignificantBits(), aiModelUpdateMsg.getIdLSB());
}
@Test
public void testSendAiModelToCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
}
@Test
public void testUpdateAiModelNameOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkAiModelOnCloud(uplinkMsg, uuid, aiModel.getName());
aiModel.setName(UPDATED_AI_MODEL_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkAiModelOnCloud(updatedUplinkMsg, uuid, aiModel.getName());
}
@Test
public void testAiModelToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
AiModel aiModel = createSimpleAiModel(DEFAULT_AI_MODEL_NAME);
edgeImitator.expectMessageAmount(1);
AiModel savedAiModel = doPost("/api/ai/model", aiModel, AiModel.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, aiModel, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<AiModelUpdateMsg> aiModelUpdateMsgOpt = edgeImitator.findMessageByType(AiModelUpdateMsg.class);
Assert.assertTrue(aiModelUpdateMsgOpt.isPresent());
AiModelUpdateMsg latestAiModelUpdateMsg = aiModelUpdateMsgOpt.get();
AiModel aiModelFromMsg = JacksonUtil.fromString(latestAiModelUpdateMsg.getEntity(), AiModel.class, true);
Assert.assertNotNull(aiModelFromMsg);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromMsg.getName());
Assert.assertNotEquals(savedAiModel.getUuidId(), uuid);
AiModel aiModelFromCloud = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModelFromCloud);
Assert.assertNotEquals(DEFAULT_AI_MODEL_NAME, aiModelFromCloud.getName());
}
private AiModel createSimpleAiModel(String name) {
AiModel aiModel = new AiModel();
aiModel.setTenantId(tenantId);
aiModel.setName(name);
aiModel.setConfiguration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig(null, "test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build());
return aiModel;
}
private UplinkMsg getUplinkMsg(UUID uuid, AiModel aiModel, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
AiModelUpdateMsg.Builder aiModelUpdateMsgBuilder = AiModelUpdateMsg.newBuilder();
aiModelUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
aiModelUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
aiModelUpdateMsgBuilder.setEntity(JacksonUtil.toString(aiModel));
aiModelUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(aiModelUpdateMsgBuilder);
uplinkMsgBuilder.addAiModelUpdateMsg(aiModelUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkAiModelOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
AiModel aiModel = doGet("/api/ai/model/" + uuid, AiModel.class);
Assert.assertNotNull(aiModel);
Assert.assertEquals(resourceTitle, aiModel.getName());
}
}

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -29,6 +29,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient; import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.controller.AbstractWebTest; import org.thingsboard.server.controller.AbstractWebTest;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
@ -358,6 +359,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg)); result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
} }
} }
if (downlinkMsg.getAiModelUpdateMsgCount() > 0) {
for (AiModelUpdateMsg aiModelUpdateMsg : downlinkMsg.getAiModelUpdateMsgList()) {
result.add(saveDownlinkMsg(aiModelUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) { if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
} }

4
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult;
@ -222,8 +223,7 @@ public class PropagationCalculatedFieldStateTest {
private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) { private CalculatedFieldConfiguration getCalculatedFieldConfig(boolean applyExpressionToResolvedArguments) {
var config = new PropagationCalculatedFieldConfiguration(); var config = new PropagationCalculatedFieldConfiguration();
config.setDirection(EntitySearchDirection.TO); config.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
config.setRelationType(EntityRelation.CONTAINS_TYPE);
config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments); config.setApplyExpressionToResolvedArguments(applyExpressionToResolvedArguments);
Argument temperatureArg = new Argument(); Argument temperatureArg = new Argument();

77
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -27,7 +27,7 @@ import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -39,17 +39,19 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -87,9 +89,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -106,12 +105,10 @@ import org.thingsboard.server.service.system.DefaultSystemInfoService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -234,8 +231,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
}); });
JsonNode attr = JacksonUtil.newObjectNode() JsonNode attr = JacksonUtil.newObjectNode()
.set("bool", BooleanNode.TRUE); .set("createAlarm", BooleanNode.TRUE);
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString());
await().atMost(10, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
@ -341,8 +338,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
JsonNode attr = JacksonUtil.newObjectNode() JsonNode attr = JacksonUtil.newObjectNode()
.set("bool", BooleanNode.TRUE); .set("createAlarm", BooleanNode.TRUE);
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr); postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString());
await().atMost(10, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null); .until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
@ -944,35 +941,27 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) {
DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); DeviceProfile deviceProfile = createDeviceProfile("For notification rule test");
deviceProfile.setTenantId(tenantId); deviceProfile.setTenantId(tenantId);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
List<DeviceProfileAlarm> alarms = new ArrayList<>(); CalculatedField alarmCf = new CalculatedField();
DeviceProfileAlarm alarm = new DeviceProfileAlarm(); alarmCf.setType(CalculatedFieldType.ALARM);
alarm.setAlarmType(alarmType); alarmCf.setEntityId(deviceProfile.getId());
alarm.setId(alarmType); alarmCf.setName(alarmType);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
configuration.setArguments(Map.of("createAlarm", argument));
AlarmRule alarmRule = new AlarmRule(); AlarmRule alarmRule = new AlarmRule();
alarmRule.setAlarmDetails("Details"); SimpleAlarmCondition condition = new SimpleAlarmCondition();
AlarmCondition alarmCondition = new AlarmCondition(); TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
alarmCondition.setSpec(new SimpleAlarmConditionSpec()); expression.setExpression("return createAlarm == true;");
List<AlarmConditionFilter> condition = new ArrayList<>(); condition.setExpression(expression);
alarmRule.setCondition(condition);
AlarmConditionFilter alarmConditionFilter = new AlarmConditionFilter(); configuration.setCreateRules(Map.of(
alarmConditionFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, "bool")); AlarmSeverity.CRITICAL, alarmRule
BooleanFilterPredicate predicate = new BooleanFilterPredicate(); ));
predicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); alarmCf.setConfiguration(configuration);
predicate.setValue(new FilterPredicateValue<>(true)); saveCalculatedField(alarmCf);
alarmConditionFilter.setPredicate(predicate);
alarmConditionFilter.setValueType(EntityKeyValueType.BOOLEAN);
condition.add(alarmConditionFilter);
alarmCondition.setCondition(condition);
alarmRule.setCondition(alarmCondition);
TreeMap<AlarmSeverity, AlarmRule> createRules = new TreeMap<>();
createRules.put(AlarmSeverity.CRITICAL, alarmRule);
alarm.setCreateRules(createRules);
alarms.add(alarm);
deviceProfile.getProfileData().setAlarms(alarms);
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
return deviceProfile; return deviceProfile;
} }

4
common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java

@ -29,6 +29,8 @@ public interface AiModelService extends EntityDaoService {
AiModel save(AiModel model); AiModel save(AiModel model);
AiModel save(AiModel model, boolean doValidate);
Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId); Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink); PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
@ -37,6 +39,8 @@ public interface AiModelService extends EntityDaoService {
FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId); FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
Optional<AiModel> findAiModelByTenantIdAndName(TenantId tenantId, String name);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId); boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
} }

4
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -51,10 +51,6 @@ import java.util.UUID;
public interface AlarmService extends EntityDaoService { public interface AlarmService extends EntityDaoService {
/*
* New API, since 3.5.
*/
/** /**
* Designed for atomic operations over active alarms. * Designed for atomic operations over active alarms.
* Only one active alarm may exist for the pair {originatorId, alarmType} * Only one active alarm may exist for the pair {originatorId, alarmType}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetInfo;
@ -48,6 +49,8 @@ public interface AssetService extends EntityDaoService {
Asset saveAsset(Asset asset); Asset saveAsset(Asset asset);
Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId); Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, CustomerId customerId);
Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId); Asset unassignAssetFromCustomer(TenantId tenantId, AssetId assetId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.customer;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -37,6 +38,8 @@ public interface CustomerService extends EntityDaoService {
Customer saveCustomer(Customer customer); Customer saveCustomer(Customer customer);
Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy);
void deleteCustomer(TenantId tenantId, CustomerId customerId); void deleteCustomer(TenantId tenantId, CustomerId customerId);
Customer findOrCreatePublicCustomer(TenantId tenantId); Customer findOrCreatePublicCustomer(TenantId tenantId);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -58,8 +59,12 @@ public interface DeviceService extends EntityDaoService {
Device saveDeviceWithAccessToken(Device device, String accessToken); Device saveDeviceWithAccessToken(Device device, String accessToken);
Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy);
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials); Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials);
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy);
Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile); Device saveDevice(ProvisionRequest provisionRequest, DeviceProfile profile);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId); Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
@ -38,6 +39,8 @@ public interface EntityViewService extends EntityDaoService {
EntityView saveEntityView(EntityView entityView); EntityView saveEntityView(EntityView entityView);
EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy);
EntityView saveEntityView(EntityView entityView, boolean doValidate); EntityView saveEntityView(EntityView entityView, boolean doValidate);
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId); EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, CustomerId customerId);

23
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictPolicy.java

@ -0,0 +1,23 @@
/**
* 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.common.data;
public enum NameConflictPolicy {
FAIL,
UNIQUIFY;
}

25
common/data/src/main/java/org/thingsboard/server/common/data/NameConflictStrategy.java

@ -0,0 +1,25 @@
/**
* 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.common.data;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema
public record NameConflictStrategy(NameConflictPolicy policy, String separator, UniquifyStrategy uniquifyStrategy) {
public static final NameConflictStrategy DEFAULT = new NameConflictStrategy(NameConflictPolicy.FAIL, null, null);
}

23
common/data/src/main/java/org/thingsboard/server/common/data/UniquifyStrategy.java

@ -0,0 +1,23 @@
/**
* 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.common.data;
public enum UniquifyStrategy {
RANDOM,
INCREMENTAL;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule; package org.thingsboard.server.common.data.alarm.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@ -30,6 +31,7 @@ public class AlarmRule {
private String alarmDetails; private String alarmDetails;
private DashboardId dashboardId; private DashboardId dashboardId;
@JsonIgnore
public boolean requiresScheduledReevaluation() { public boolean requiresScheduledReevaluation() {
return condition.hasSchedule(); return condition.hasSchedule();
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java

@ -45,6 +45,7 @@ public abstract class AlarmCondition {
@Valid @Valid
private AlarmConditionValue<AlarmSchedule> schedule; private AlarmConditionValue<AlarmSchedule> schedule;
@JsonIgnore
public boolean hasSchedule() { public boolean hasSchedule() {
return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule);
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition; package org.thingsboard.server.common.data.alarm.rule.condition;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -27,4 +29,10 @@ public class AlarmConditionValue<T> {
private T staticValue; private T staticValue;
private String dynamicValueArgument; private String dynamicValueArgument;
@JsonIgnore
@AssertTrue(message = "Either staticValue or dynamicValueArgument must be set")
public boolean isValid() {
return staticValue != null ^ dynamicValueArgument != null;
}
} }

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java

@ -15,21 +15,22 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression; package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import java.io.Serializable; import java.io.Serializable;
@Schema
@Data @Data
public class AlarmConditionFilter implements Serializable { public class AlarmConditionFilter implements Serializable {
@NotBlank @NotBlank
private String argument; private String argument;
@NotNull
private EntityKeyValueType valueType;
@Valid @Valid
@NotNull @NotNull
private KeyFilterPredicate predicate; private KeyFilterPredicate predicate;

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class BooleanFilterPredicate implements SimpleKeyFilterPredicate<Boolean> { public class BooleanFilterPredicate implements SimpleKeyFilterPredicate<Boolean> {
@NotNull
private BooleanOperation operation; private BooleanOperation operation;
@Valid
@NotNull
private AlarmConditionValue<Boolean> value; private AlarmConditionValue<Boolean> value;
@Override @Override

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> { public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@NotNull
private NumericOperation operation; private NumericOperation operation;
@Valid
@NotNull
private AlarmConditionValue<Double> value; private AlarmConditionValue<Double> value;
@Override @Override

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> { public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
@NotNull
private StringOperation operation; private StringOperation operation;
@Valid
@NotNull
private AlarmConditionValue<String> value; private AlarmConditionValue<String> value;
private boolean ignoreCase; private boolean ignoreCase;
@ -40,4 +45,5 @@ public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
IN, IN,
NOT_IN NOT_IN
} }
} }

35
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java

@ -15,15 +15,24 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule; import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import static java.util.Map.Entry.comparingByKey;
@Data @Data
public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration {
@ -51,15 +60,31 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat
return null; return null;
} }
@JsonIgnore
@Override @Override
public void validate() { public boolean requiresScheduledReevaluation() {
return getAllRules().anyMatch(entry -> entry.getValue().requiresScheduledReevaluation());
}
@JsonIgnore
public Stream<Pair<AlarmSeverity, AlarmRule>> getAllRules() {
Stream<Pair<AlarmSeverity, AlarmRule>> rules = createRules.entrySet().stream()
.map(entry -> Pair.of(entry.getKey(), entry.getValue()));
if (clearRule != null) {
rules = Stream.concat(rules, Stream.of(Pair.of(null, clearRule)));
}
return rules.sorted(comparingByKey(Comparator.nullsLast(Comparator.naturalOrder())));
} }
@Override public boolean rulesEqual(AlarmCalculatedFieldConfiguration other, BiPredicate<AlarmRule, AlarmRule> equalityCheck) {
public boolean requiresScheduledReevaluation() { List<Pair<AlarmSeverity, AlarmRule>> thisRules = this.getAllRules().toList();
return createRules.values().stream().anyMatch(AlarmRule::requiresScheduledReevaluation) || List<Pair<AlarmSeverity, AlarmRule>> otherRules = other.getAllRules().toList();
(clearRule != null && clearRule.requiresScheduledReevaluation()); return CollectionsUtil.elementsEqual(thisRules, otherRules, (thisRule, otherRule) -> {
if (!Objects.equals(thisRule.getKey(), otherRule.getKey())) {
return false;
}
return equalityCheck.test(thisRule.getValue(), otherRule.getValue());
});
} }
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java

@ -50,7 +50,7 @@ public interface CalculatedFieldConfiguration {
Output getOutput(); Output getOutput();
void validate(); default void validate() {}
@JsonIgnore @JsonIgnore
default List<EntityId> getReferencedEntities() { default List<EntityId> getReferencedEntities() {
@ -72,6 +72,7 @@ public interface CalculatedFieldConfiguration {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@JsonIgnore
default boolean requiresScheduledReevaluation() { default boolean requiresScheduledReevaluation() {
return false; return false;
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfiguration.java

@ -15,13 +15,12 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationPathLevel;
import java.util.List; import java.util.List;
@ -32,10 +31,9 @@ public class PropagationCalculatedFieldConfiguration extends BaseCalculatedField
public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx"; public static final String PROPAGATION_CONFIG_ARGUMENT = "propagationCtx";
@Valid
@NotNull @NotNull
private EntitySearchDirection direction; private RelationPathLevel relation;
@NotBlank
private String relationType;
private boolean applyExpressionToResolvedArguments; private boolean applyExpressionToResolvedArguments;
@ -77,7 +75,7 @@ public class PropagationCalculatedFieldConfiguration extends BaseCalculatedField
public Argument toPropagationArgument() { public Argument toPropagationArgument() {
var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(direction, relationType))); refDynamicSourceConfiguration.setLevels(List.of(relation));
var propagationArgument = new Argument(); var propagationArgument = new Argument();
propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); propagationArgument.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
return propagationArgument; return propagationArgument;

3
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java

@ -47,7 +47,8 @@ public enum EdgeEventType {
TB_RESOURCE(true, EntityType.TB_RESOURCE), TB_RESOURCE(true, EntityType.TB_RESOURCE),
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN), DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD); CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD),
AI_MODEL(true, EntityType.AI_MODEL);
private final boolean allEdgesRelated; private final boolean allEdgesRelated;

2
common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID; import java.util.UUID;
@ToString @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedFieldDebugEvent extends Event { public class CalculatedFieldDebugEvent extends Event {

1
common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java

@ -113,6 +113,7 @@ public class EntityIdFactory {
case OAUTH2_CLIENT -> new OAuth2ClientId(uuid); case OAUTH2_CLIENT -> new OAuth2ClientId(uuid);
case DOMAIN -> new DomainId(uuid); case DOMAIN -> new DomainId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case AI_MODEL -> new AiModelId(uuid);
default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); default -> throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
}; };
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java

@ -15,9 +15,11 @@
*/ */
package org.thingsboard.server.common.data.relation; package org.thingsboard.server.common.data.relation;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
public record RelationPathLevel(EntitySearchDirection direction, String relationType) { public record RelationPathLevel(@NotNull EntitySearchDirection direction, @NotBlank String relationType) {
public void validate() { public void validate() {
if (direction == null) { if (direction == null) {

29
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -18,9 +18,11 @@ package org.thingsboard.server.common.data.util;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class CollectionsUtil { public class CollectionsUtil {
@ -95,4 +97,31 @@ public class CollectionsUtil {
return false; return false;
} }
public static <T> boolean elementsEqual(Iterable<T> iterable1, Iterable<T> iterable2, BiPredicate<T, T> equalityCheck) {
if (iterable1 instanceof Collection<?> collection1 && iterable2 instanceof Collection<?> collection2) {
if (collection1.size() != collection2.size()) {
return false;
}
}
Iterator<T> iterator1 = iterable1.iterator();
Iterator<T> iterator2 = iterable2.iterator();
while (true) {
if (iterator1.hasNext()) {
if (!iterator2.hasNext()) {
return false;
}
T o1 = iterator1.next();
T o2 = iterator2.next();
if (equalityCheck.test(o1, o2)) {
continue;
} else {
return false;
}
}
return !iterator2.hasNext();
}
}
} }

12
common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.util; package org.thingsboard.server.common.data.util;
import com.google.gson.JsonParser;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
@ -40,6 +41,11 @@ public class TypeCastUtil {
} catch (RuntimeException ignored) {} } catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value)); return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value));
} else if (looksLikeJson(value)) {
try {
return Pair.of(DataType.JSON, JsonParser.parseString(value));
} catch (Exception ignored) {
}
} }
return Pair.of(DataType.STRING, value); return Pair.of(DataType.STRING, value);
} }
@ -70,4 +76,10 @@ public class TypeCastUtil {
return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e"); return valueAsString.contains(".") && !valueAsString.contains("E") && !valueAsString.contains("e");
} }
private static boolean looksLikeJson(String value) {
String trimmed = value.trim();
return (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"));
}
} }

5
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/PropagationCalculatedFieldConfigurationTest.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -95,6 +96,7 @@ public class PropagationCalculatedFieldConfigurationTest {
@Test @Test
void validateShouldThrowWhenReferencedEntityKeyIsNotSet() { void validateShouldThrowWhenReferencedEntityKeyIsNotSet() {
var cfg = new PropagationCalculatedFieldConfiguration(); var cfg = new PropagationCalculatedFieldConfiguration();
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
Argument argument = new Argument(); Argument argument = new Argument();
cfg.setArguments(Map.of("someArgumentName", argument)); cfg.setArguments(Map.of("someArgumentName", argument));
assertThatThrownBy(cfg::validate) assertThatThrownBy(cfg::validate)
@ -128,8 +130,7 @@ public class PropagationCalculatedFieldConfigurationTest {
@Test @Test
void validateToPropagationArgumentMethodCallReturnCorrectArgument() { void validateToPropagationArgumentMethodCallReturnCorrectArgument() {
var cfg = new PropagationCalculatedFieldConfiguration(); var cfg = new PropagationCalculatedFieldConfiguration();
cfg.setDirection(EntitySearchDirection.TO); cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
Argument propagationArgument = cfg.toPropagationArgument(); Argument propagationArgument = cfg.toPropagationArgument();
assertThat(propagationArgument).isNotNull(); assertThat(propagationArgument).isNotNull();

2
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -136,7 +136,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
.setConnectRequestMsg(ConnectRequestMsg.newBuilder() .setConnectRequestMsg(ConnectRequestMsg.newBuilder()
.setEdgeRoutingKey(edgeKey) .setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret) .setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_4_2_0) .setEdgeVersion(EdgeVersion.V_4_3_0)
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build()) .build())
.build()); .build());

9
common/edge-api/src/main/proto/edge.proto

@ -44,6 +44,7 @@ enum EdgeVersion {
V_4_0_0 = 10; V_4_0_0 = 10;
V_4_1_0 = 11; V_4_1_0 = 11;
V_4_2_0 = 12; V_4_2_0 = 12;
V_4_3_0 = 13;
V_LATEST = 999; V_LATEST = 999;
} }
@ -133,6 +134,12 @@ message CalculatedFieldUpdateMsg{
string entity = 4; string entity = 4;
} }
message AiModelUpdateMsg{
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string entity = 4;
}
message EntityDataProto { message EntityDataProto {
int64 entityIdMSB = 1; int64 entityIdMSB = 1;
@ -441,6 +448,7 @@ message UplinkMsg {
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
repeated AiModelUpdateMsg aiModelUpdateMsg = 27;
} }
message UplinkResponseMsg { message UplinkResponseMsg {
@ -491,4 +499,5 @@ message DownlinkMsg {
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
repeated AiModelUpdateMsg aiModelUpdateMsg = 36;
} }

2
common/proto/src/main/proto/queue.proto

@ -1724,12 +1724,10 @@ message ToCalculatedFieldMsg {
CalculatedFieldTelemetryMsgProto telemetryMsg = 1; CalculatedFieldTelemetryMsgProto telemetryMsg = 1;
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2;
EntityActionEventProto eventMsg = 3; EntityActionEventProto eventMsg = 3;
repeated string cfTypes = 4;
} }
message ToCalculatedFieldNotificationMsg { message ToCalculatedFieldNotificationMsg {
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1;
repeated string cfTypes = 2;
} }
/* Messages that are handled by ThingsBoard RuleEngine Service */ /* Messages that are handled by ThingsBoard RuleEngine Service */

5
dao/src/main/java/org/thingsboard/server/dao/Dao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao; package org.thingsboard.server.dao;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.edqs.fields.EntityFields;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -32,6 +33,10 @@ public interface Dao<T> {
ListenableFuture<T> findByIdAsync(TenantId tenantId, UUID id); ListenableFuture<T> findByIdAsync(TenantId tenantId, UUID id);
default List<EntityInfo> findEntityInfosByNamePrefix(TenantId tenantId, String name) {
throw new UnsupportedOperationException();
}
boolean existsById(TenantId tenantId, UUID id); boolean existsById(TenantId tenantId, UUID id);
ListenableFuture<Boolean> existsByIdAsync(TenantId tenantId, UUID id); ListenableFuture<Boolean> existsByIdAsync(TenantId tenantId, UUID id);

41
dao/src/main/java/org/thingsboard/server/dao/ai/AiModelServiceImpl.java

@ -29,12 +29,15 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.entity.CachedVersionedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.model.sql.AiModelEntity; import org.thingsboard.server.dao.model.sql.AiModelEntity;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -63,11 +66,23 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
@Override @Override
@Transactional @Transactional
public AiModel save(AiModel model) { public AiModel save(AiModel model) {
aiModelValidator.validate(model, AiModel::getTenantId); return save(model, true);
}
@Override
public AiModel save(AiModel aiModel, boolean doValidate) {
AiModel oldAiModel = null;
if (doValidate) {
oldAiModel = aiModelValidator.validate(aiModel, AiModel::getTenantId);
} else if (aiModel.getId() != null) {
oldAiModel = findAiModelById(aiModel.getTenantId(), aiModel.getId()).orElse(null);
}
AiModel savedModel; AiModel savedModel;
try { try {
savedModel = aiModelDao.saveAndFlush(model.getTenantId(), model); savedModel = aiModelDao.saveAndFlush(aiModel.getTenantId(), aiModel);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedModel.getTenantId()).entityId(savedModel.getId())
.entity(savedModel).oldEntity(oldAiModel).created(oldAiModel == null).broadcastEvent(true).build());
} catch (Exception e) { } catch (Exception e) {
checkConstraintViolation(e, checkConstraintViolation(e,
"ai_model_name_unq_key", "AI model with such name already exist!", "ai_model_name_unq_key", "AI model with such name already exist!",
@ -103,10 +118,15 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId))); return FluentFuture.from(jpaExecutor.submit(() -> findAiModelByTenantIdAndId(tenantId, modelId)));
} }
@Override
public Optional<AiModel> findAiModelByTenantIdAndName(TenantId tenantId, String name) {
return Optional.ofNullable(aiModelDao.findByTenantIdAndName(tenantId.getId(), name));
}
@Override @Override
@Transactional @Transactional
public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) { public boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId) {
return deleteByTenantIdAndIdInternal(tenantId, modelId); return deleteByTenantIdAndIdInternal(tenantId, modelId.getId());
} }
@Override @Override
@ -123,14 +143,21 @@ class AiModelServiceImpl extends CachedVersionedEntityService<AiModelCacheKey, A
@Override @Override
@Transactional @Transactional
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteByTenantIdAndIdInternal(tenantId, new AiModelId(id.getId())); deleteByTenantIdAndIdInternal(tenantId, id.getId());
} }
private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, AiModelId modelId) { private boolean deleteByTenantIdAndIdInternal(TenantId tenantId, UUID modelId) {
boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, modelId); AiModel aiModel = findAiModelById(tenantId, new AiModelId(modelId)).orElse(null);
if (aiModel == null) {
return false;
}
boolean deleted = aiModelDao.deleteByTenantIdAndId(tenantId, aiModel.getId());
if (deleted) { if (deleted) {
publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, modelId))); publishEvictEvent(new AiModelCacheEvictEvent.Deleted(AiModelCacheKey.of(tenantId, aiModel.getId())));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(aiModel.getId()).entity(aiModel).build());
} }
return deleted; return deleted;
} }

2
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -159,7 +159,7 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override @Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) { public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) {
var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details));
if (result.getAlarm() != null) { if (pushEvent && result.getAlarm() != null) {
eventPublisher.publishEvent(ActionEntityEvent.builder() eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(result.getAlarm().getId()) .entityId(result.getAlarm().getId())

20
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -26,6 +26,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
@ -146,14 +148,24 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return saveAsset(asset, true); return saveAsset(asset, true);
} }
@Override
public Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy) {
return saveAsset(asset, true, nameConflictStrategy);
}
@Override @Override
public Asset saveAsset(Asset asset, boolean doValidate) { public Asset saveAsset(Asset asset, boolean doValidate) {
return saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT);
}
private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveAsset [{}]", asset); log.trace("Executing saveAsset [{}]", asset);
Asset oldAsset = null; Asset oldAsset = (asset.getId() != null) ? assetDao.findById(asset.getTenantId(), asset.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldAsset == null || !oldAsset.getName().equals(asset.getName()))) {
uniquifyEntityName(asset, oldAsset, asset::setName, EntityType.ASSET, nameConflictStrategy);
}
if (doValidate) { if (doValidate) {
oldAsset = assetValidator.validate(asset, Asset::getTenantId); assetValidator.validate(asset, Asset::getTenantId);
} else if (asset.getId() != null) {
oldAsset = findAssetById(asset.getTenantId(), asset.getId());
} }
AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null); AssetCacheEvictEvent evictEvent = new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), oldAsset != null ? oldAsset.getName() : null);
Asset savedAsset; Asset savedAsset;

27
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -28,6 +28,8 @@ import org.thingsboard.server.cache.customer.CustomerCacheEvictEvent;
import org.thingsboard.server.cache.customer.CustomerCacheKey; import org.thingsboard.server.cache.customer.CustomerCacheKey;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -139,20 +141,29 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Override @Override
@Transactional @Transactional
public Customer saveCustomer(Customer customer) { public Customer saveCustomer(Customer customer) {
return saveCustomer(customer, true); return saveCustomer(customer, true, NameConflictStrategy.DEFAULT);
}
@Override
@Transactional
public Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy) {
return saveCustomer(customer, true, nameConflictStrategy);
} }
private Customer saveCustomer(Customer customer, boolean doValidate) { private Customer saveCustomer(Customer customer, boolean doValidate) {
return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT);
}
private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveCustomer [{}]", customer); log.trace("Executing saveCustomer [{}]", customer);
Customer oldCustomer = null; Customer oldCustomer = (customer.getId() != null) ? customerDao.findById(customer.getTenantId(), customer.getId().getId()) : null;
String oldCustomerTitle = null; if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldCustomer == null || !oldCustomer.getTitle().equals(customer.getTitle()))) {
if (doValidate) { uniquifyEntityName(customer, oldCustomer, customer::setTitle, EntityType.CUSTOMER, nameConflictStrategy);
oldCustomer = customerValidator.validate(customer, Customer::getTenantId);
if (oldCustomer != null) {
oldCustomerTitle = oldCustomer.getTitle();
} }
if (doValidate) {
customerValidator.validate(customer, Customer::getTenantId);
} }
var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomerTitle); var evictEvent = new CustomerCacheEvictEvent(customer.getTenantId(), customer.getTitle(), oldCustomer != null ? oldCustomer.getTitle() : null);
try { try {
Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer); Customer savedCustomer = customerDao.saveAndFlush(customer.getTenantId(), customer);
if (!savedCustomer.isPublic()) { if (!savedCustomer.isPublic()) {

33
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -39,6 +39,8 @@ import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
@ -167,6 +169,12 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
return doSaveDevice(device, accessToken, true); return doSaveDevice(device, accessToken, true);
} }
@Transactional
@Override
public Device saveDeviceWithAccessToken(Device device, String accessToken, NameConflictStrategy nameConflictStrategy) {
return doSaveDevice(device, accessToken, true, nameConflictStrategy);
}
@Override @Override
public Device saveDevice(Device device, boolean doValidate) { public Device saveDevice(Device device, boolean doValidate) {
return doSaveDevice(device, null, doValidate); return doSaveDevice(device, null, doValidate);
@ -181,7 +189,13 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
@Transactional @Transactional
@Override @Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) {
Device savedDevice = this.saveDeviceWithoutCredentials(device, true); return this.saveDeviceWithCredentials(device, deviceCredentials, NameConflictStrategy.DEFAULT);
}
@Transactional
@Override
public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, NameConflictStrategy nameConflictStrategy) {
Device savedDevice = this.saveDeviceWithoutCredentials(device, true, nameConflictStrategy);
deviceCredentials.setDeviceId(savedDevice.getId()); deviceCredentials.setDeviceId(savedDevice.getId());
if (device.getId() == null) { if (device.getId() == null) {
deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials);
@ -198,7 +212,11 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
} }
private Device doSaveDevice(Device device, String accessToken, boolean doValidate) { private Device doSaveDevice(Device device, String accessToken, boolean doValidate) {
Device savedDevice = this.saveDeviceWithoutCredentials(device, doValidate); return doSaveDevice(device, accessToken, doValidate, NameConflictStrategy.DEFAULT);
}
private Device doSaveDevice(Device device, String accessToken, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
Device savedDevice = this.saveDeviceWithoutCredentials(device, doValidate, nameConflictStrategy);
if (device.getId() == null) { if (device.getId() == null) {
DeviceCredentials deviceCredentials = new DeviceCredentials(); DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId()));
@ -209,13 +227,14 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
return savedDevice; return savedDevice;
} }
private Device saveDeviceWithoutCredentials(Device device, boolean doValidate) { private Device saveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveDevice [{}]", device); log.trace("Executing saveDevice [{}]", device);
Device oldDevice = null; Device oldDevice = (device.getId() != null) ? deviceDao.findById(device.getTenantId(), device.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldDevice == null || !oldDevice.getName().equals(device.getName()))) {
uniquifyEntityName(device, oldDevice, device::setName, EntityType.DEVICE, nameConflictStrategy);
}
if (doValidate) { if (doValidate) {
oldDevice = deviceValidator.validate(device, Device::getTenantId); deviceValidator.validate(device, Device::getTenantId);
} else if (device.getId() != null) {
oldDevice = findDeviceById(device.getTenantId(), device.getId());
} }
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null); DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), oldDevice != null ? oldDevice.getName() : null);
try { try {

44
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -22,15 +22,22 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasDebugSettings; import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
@ -44,7 +51,12 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.UniquifyStrategy.RANDOM;
@Slf4j @Slf4j
public abstract class AbstractEntityService { public abstract class AbstractEntityService {
@ -83,6 +95,9 @@ public abstract class AbstractEntityService {
@Lazy @Lazy
protected TbTenantProfileCache tbTenantProfileCache; protected TbTenantProfileCache tbTenantProfileCache;
@Autowired
protected EntityDaoRegistry entityDaoRegistry;
@Value("${debug.settings.default_duration:15}") @Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes; private int defaultDebugDurationMinutes;
@ -155,4 +170,33 @@ public abstract class AbstractEntityService {
private long getMaxDebugAllUntil(TenantId tenantId, long now) { private long getMaxDebugAllUntil(TenantId tenantId, long now) {
return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes)); return now + TimeUnit.MINUTES.toMillis(DebugModeUtil.getMaxDebugAllDuration(tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxDebugModeDurationMinutes(), defaultDebugDurationMinutes));
} }
protected <E extends HasId<?> & HasTenantId & HasName> void uniquifyEntityName(E entity, E oldEntity, Consumer<String> setName, EntityType entityType, NameConflictStrategy strategy) {
Dao<?> dao = entityDaoRegistry.getDao(entityType);
List<EntityInfo> existingEntities = dao.findEntityInfosByNamePrefix(entity.getTenantId(), entity.getName());
Set<String> existingNames = existingEntities.stream()
.filter(e -> (oldEntity == null || !e.getId().equals(oldEntity.getId())))
.map(EntityInfo::getName)
.collect(Collectors.toSet());
if (existingNames.contains(entity.getName())) {
String uniqueName = generateUniqueName(entity.getName(), existingNames, strategy);
setName.accept(uniqueName);
}
}
private String generateUniqueName(String baseName, Set<String> existingNames, NameConflictStrategy strategy) {
String newName;
int index = 1;
String separator = strategy.separator();
boolean isRandom = strategy.uniquifyStrategy() == RANDOM;
do {
String suffix = isRandom ? StringUtils.randomAlphanumeric(6) : String.valueOf(index++);
newName = baseName + separator + suffix;
} while (existingNames.contains(newName));
return newName;
}
} }

20
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.NameConflictPolicy;
import org.thingsboard.server.common.data.NameConflictStrategy;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -110,14 +112,24 @@ public class EntityViewServiceImpl extends CachedVersionedEntityService<EntityVi
return saveEntityView(entityView, true); return saveEntityView(entityView, true);
} }
@Override
public EntityView saveEntityView(EntityView entityView, NameConflictStrategy nameConflictStrategy) {
return saveEntityView(entityView, true, nameConflictStrategy);
}
@Override @Override
public EntityView saveEntityView(EntityView entityView, boolean doValidate) { public EntityView saveEntityView(EntityView entityView, boolean doValidate) {
return saveEntityView(entityView, doValidate, NameConflictStrategy.DEFAULT);
}
private EntityView saveEntityView(EntityView entityView, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing save entity view [{}]", entityView); log.trace("Executing save entity view [{}]", entityView);
EntityView old = null; EntityView old = (entityView.getId() != null) ? entityViewDao.findById(entityView.getTenantId(), entityView.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (old == null || !entityView.getName().equals(old.getName()))) {
uniquifyEntityName(entityView, old, entityView::setName, EntityType.ENTITY_VIEW, nameConflictStrategy);
}
if (doValidate) { if (doValidate) {
old = entityViewValidator.validate(entityView, EntityView::getTenantId); entityViewValidator.validate(entityView, EntityView::getTenantId);
} else if (entityView.getId() != null) {
old = findEntityViewById(entityView.getTenantId(), entityView.getId(), false);
} }
try { try {
EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView); EntityView saved = entityViewDao.save(entityView.getTenantId(), entityView);

5
dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java

@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.AssetFields; import org.thingsboard.server.common.data.edqs.fields.AssetFields;
import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
@ -103,6 +104,10 @@ public interface AssetRepository extends JpaRepository<AssetEntity, UUID>, Expor
AssetEntity findByTenantIdAndName(UUID tenantId, String name); AssetEntity findByTenantIdAndName(UUID tenantId, String name);
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ASSET', a.name) " +
"FROM AssetEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')")
List<EntityInfo> findEntityInfosByNamePrefix(UUID tenantId, String prefix);
@Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " +
"AND a.type = :type " + "AND a.type = :type " +
"AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " + "AND (:textSearch IS NULL OR ilike(a.name, CONCAT('%', :textSearch, '%')) = true " +

7
dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java

@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit; import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
@ -267,6 +268,12 @@ public class JpaAssetDao extends JpaAbstractDao<AssetEntity, Asset> implements A
return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink)); return nativeAssetRepository.findProfileEntityIdInfosByTenantId(tenantId, DaoUtil.toPageable(pageLink));
} }
@Override
public List<EntityInfo> findEntityInfosByNamePrefix(TenantId tenantId, String name) {
log.debug("Find asset entity infos by name [{}]", name);
return assetRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
}
@Override @Override
public Long countByTenantId(TenantId tenantId) { public Long countByTenantId(TenantId tenantId) {
return assetRepository.countByTenantId(tenantId.getId()); return assetRepository.countByTenantId(tenantId.getId());

3
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java

@ -19,7 +19,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
@ -30,7 +29,7 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
CalculatedFieldEntity findByEntityIdAndTypeAndName(UUID entityId, CalculatedFieldType type, String name); CalculatedFieldEntity findByEntityIdAndTypeAndName(UUID entityId, String type, String name);
List<CalculatedFieldId> findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId); List<CalculatedFieldId> findCalculatedFieldIdsByTenantIdAndEntityId(UUID tenantId, UUID entityId);

2
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java

@ -69,7 +69,7 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
@Override @Override
public CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name) { public CalculatedField findByEntityIdAndTypeAndName(EntityId entityId, CalculatedFieldType type, String name) {
return DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndTypeAndName(entityId.getId(), type, name)); return DaoUtil.getData(calculatedFieldRepository.findByEntityIdAndTypeAndName(entityId.getId(), type.name(), name));
} }
@Override @Override

5
dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java

@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.common.data.edqs.fields.CustomerFields;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.CustomerEntity; import org.thingsboard.server.dao.model.sql.CustomerEntity;
@ -41,6 +42,10 @@ public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID>,
CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title); CustomerEntity findByTenantIdAndTitle(UUID tenantId, String title);
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'CUSTOMER', a.title) " +
"FROM CustomerEntity a WHERE a.tenantId = :tenantId AND a.title LIKE CONCAT(:prefix, '%')")
List<EntityInfo> findEntityInfosByNamePrefix(UUID tenantId, String prefix);
@Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " + @Query(value = "SELECT * FROM customer c WHERE c.tenant_id = :tenantId " +
"AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true) "AND c.is_public IS TRUE ORDER BY c.id ASC LIMIT 1", nativeQuery = true)
CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId); CustomerEntity findPublicCustomerByTenantId(@Param("tenantId") UUID tenantId);

6
dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java

@ -20,6 +20,7 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edqs.fields.CustomerFields; import org.thingsboard.server.common.data.edqs.fields.CustomerFields;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -117,6 +118,11 @@ public class JpaCustomerDao extends JpaAbstractDao<CustomerEntity, Customer> imp
return customerRepository.findNextBatch(id, Limit.of(batchSize)); return customerRepository.findNextBatch(id, Limit.of(batchSize));
} }
@Override
public List<EntityInfo> findEntityInfosByNamePrefix(TenantId tenantId, String name) {
return customerRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
}
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.CUSTOMER; return EntityType.CUSTOMER;

5
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java

@ -22,6 +22,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.edqs.fields.DeviceFields;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.DeviceEntity; import org.thingsboard.server.dao.model.sql.DeviceEntity;
@ -151,6 +152,10 @@ public interface DeviceRepository extends JpaRepository<DeviceEntity, UUID>, Exp
DeviceEntity findByTenantIdAndName(UUID tenantId, String name); DeviceEntity findByTenantIdAndName(UUID tenantId, String name);
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'DEVICE', a.name) " +
"FROM DeviceEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')")
List<EntityInfo> findEntityInfosByNamePrefix(UUID tenantId, String prefix);
List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List<UUID> deviceIds); List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(UUID tenantId, UUID customerId, List<UUID> deviceIds);
List<DeviceEntity> findDevicesByTenantIdAndIdIn(UUID tenantId, List<UUID> deviceIds); List<DeviceEntity> findDevicesByTenantIdAndIdIn(UUID tenantId, List<UUID> deviceIds);

6
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.DeviceInfoFilter; import org.thingsboard.server.common.data.DeviceInfoFilter;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.ProfileEntityIdInfo;
@ -114,6 +115,11 @@ public class JpaDeviceDao extends JpaAbstractDao<DeviceEntity, Device> implement
DaoUtil.toPageable(pageLink))); DaoUtil.toPageable(pageLink)));
} }
@Override
public List<EntityInfo> findEntityInfosByNamePrefix(TenantId tenantId, String name) {
return deviceRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
}
@Override @Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) { public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(tenantId, deviceIds))); return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findDevicesByTenantIdAndIdIn(tenantId, deviceIds)));

5
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -21,6 +21,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewEntity;
@ -118,6 +119,10 @@ public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UU
EntityViewEntity findByTenantIdAndName(UUID tenantId, String name); EntityViewEntity findByTenantIdAndName(UUID tenantId, String name);
@Query("SELECT new org.thingsboard.server.common.data.EntityInfo(a.id, 'ENTITY_VIEW', a.name) " +
"FROM EntityViewEntity a WHERE a.tenantId = :tenantId AND a.name LIKE CONCAT(:prefix, '%')")
List<EntityInfo> findEntityInfosByNamePrefix(UUID tenantId, String prefix);
List<EntityViewEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId); List<EntityViewEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId); boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);

6
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Limit; import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
@ -230,6 +231,11 @@ public class JpaEntityViewDao extends JpaAbstractDao<EntityViewEntity, EntityVie
return entityViewRepository.findNextBatch(id, Limit.of(batchSize)); return entityViewRepository.findNextBatch(id, Limit.of(batchSize));
} }
@Override
public List<EntityInfo> findEntityInfosByNamePrefix(TenantId tenantId, String name) {
return entityViewRepository.findEntityInfosByNamePrefix(tenantId.getId(), name);
}
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.ENTITY_VIEW; return EntityType.ENTITY_VIEW;

25
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -74,6 +74,7 @@ import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.data.UsageInfo; import org.thingsboard.server.common.data.UsageInfo;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.UserEmailInfo; import org.thingsboard.server.common.data.UserEmailInfo;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
@ -98,6 +99,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.edge.EdgeInstructions; import org.thingsboard.server.common.data.edge.EdgeInstructions;
import org.thingsboard.server.common.data.edge.EdgeSearchQuery; import org.thingsboard.server.common.data.edge.EdgeSearchQuery;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery; import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -4144,6 +4146,29 @@ public class RestClient implements Closeable {
} }
} }
public AiModel saveAiModel(AiModel aiModel) {
return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody();
}
public Optional<AiModel> getAiModel(AiModelId aiModelId) {
try {
ResponseEntity<AiModel> response = restTemplate.getForEntity(
baseURL + "/api/aiModel/{aiModelId}", AiModel.class, aiModelId.getId());
return Optional.ofNullable(response.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public void deleteAiModel(AiModelId aiModelId) {
restTemplate.delete(baseURL + "/api/aiModel/{aiModelId}", aiModelId.getId());
}
private String getTimeUrlParams(TimePageLink pageLink) { private String getTimeUrlParams(TimePageLink pageLink) {
String urlParams = getUrlParams(pageLink); String urlParams = getUrlParams(pageLink);
if (pageLink.getStartTime() != null) { if (pageLink.getStartTime() != null) {

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

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

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -55,7 +55,6 @@ import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithout
@Data @Data
@Slf4j @Slf4j
class AlarmRuleState { class AlarmRuleState {
private final AlarmSeverity severity; private final AlarmSeverity severity;

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

@ -51,7 +51,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "device profile (deprecated)", // TODO: add description on why is it deprecated and what to use name = "device profile (deprecated)",
customRelations = true, customRelations = true,
relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"},
version = 1, version = 1,

9
ui-ngx/src/app/core/http/asset.service.ts

@ -15,7 +15,7 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
import { EntitySubtype } from '@shared/models/entity-type.models'; import { EntitySubtype } from '@shared/models/entity-type.models';
import { Asset, AssetInfo, AssetSearchQuery } from '@shared/models/asset.models'; import { Asset, AssetInfo, AssetSearchQuery } from '@shared/models/asset.models';
import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models';
import { SaveEntityParams } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -69,8 +70,10 @@ export class AssetService {
return this.http.get<AssetInfo>(`/api/asset/info/${assetId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<AssetInfo>(`/api/asset/info/${assetId}`, defaultHttpOptionsFromConfig(config));
} }
public saveAsset(asset: Asset, config?: RequestConfig): Observable<Asset> { public saveAsset(asset: Asset, config?: RequestConfig): Observable<Asset>;
return this.http.post<Asset>('/api/asset', asset, defaultHttpOptionsFromConfig(config)); public saveAsset(asset: Asset, saveParams: SaveEntityParams, config?: RequestConfig): Observable<Asset>;
public saveAsset(asset: Asset, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable<Asset> {
return this.http.post<Asset>('/api/asset', asset, createDefaultHttpOptions(saveParamsOrConfig, config));
} }
public deleteAsset(assetId: string, config?: RequestConfig) { public deleteAsset(assetId: string, config?: RequestConfig) {

9
ui-ngx/src/app/core/http/customer.service.ts

@ -15,12 +15,13 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { Customer } from '@shared/models/customer.model'; import { Customer } from '@shared/models/customer.model';
import { SaveEntityParams } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -40,8 +41,10 @@ export class CustomerService {
return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<Customer>(`/api/customer/${customerId}`, defaultHttpOptionsFromConfig(config));
} }
public saveCustomer(customer: Customer, config?: RequestConfig): Observable<Customer> { public saveCustomer(customer: Customer, config?: RequestConfig): Observable<Customer>;
return this.http.post<Customer>('/api/customer', customer, defaultHttpOptionsFromConfig(config)); public saveCustomer(customer: Customer, saveParams: SaveEntityParams, config?: RequestConfig): Observable<Customer>;
public saveCustomer(customer: Customer, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable<Customer> {
return this.http.post<Customer>('/api/customer', customer, createDefaultHttpOptions(saveParamsOrConfig, config));
} }
public deleteCustomer(customerId: string, config?: RequestConfig) { public deleteCustomer(customerId: string, config?: RequestConfig) {

18
ui-ngx/src/app/core/http/device.service.ts

@ -15,7 +15,7 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable, ReplaySubject } from 'rxjs'; import { Observable, ReplaySubject } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
@ -28,13 +28,15 @@ import {
DeviceInfo, DeviceInfo,
DeviceInfoQuery, DeviceInfoQuery,
DeviceSearchQuery, DeviceSearchQuery,
PublishTelemetryCommand PublishTelemetryCommand,
SaveDeviceParams
} from '@shared/models/device.models'; } from '@shared/models/device.models';
import { EntitySubtype } from '@shared/models/entity-type.models'; import { EntitySubtype } from '@shared/models/entity-type.models';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models'; import { BulkImportRequest, BulkImportResult } from '@shared/import-export/import-export.models';
import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { SaveEntityParams } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -87,15 +89,19 @@ export class DeviceService {
return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<DeviceInfo>(`/api/device/info/${deviceId}`, defaultHttpOptionsFromConfig(config));
} }
public saveDevice(device: Device, config?: RequestConfig): Observable<Device> { public saveDevice(device: Device, config?: RequestConfig): Observable<Device>;
return this.http.post<Device>('/api/device', device, defaultHttpOptionsFromConfig(config)); public saveDevice(device: Device, saveParams?: SaveDeviceParams, config?: RequestConfig): Observable<Device>;
public saveDevice(device: Device, saveParamsOrConfig?: SaveDeviceParams | RequestConfig, config?: RequestConfig): Observable<Device> {
return this.http.post<Device>('/api/device', device, createDefaultHttpOptions(saveParamsOrConfig, config));
} }
public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable<Device> { public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, config?: RequestConfig): Observable<Device>;
public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParams: SaveEntityParams, config?: RequestConfig): Observable<Device>;
public saveDeviceWithCredentials(device: Device, credentials: DeviceCredentials, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable<Device> {
return this.http.post<Device>('/api/device-with-credentials', { return this.http.post<Device>('/api/device-with-credentials', {
device, device,
credentials credentials
}, defaultHttpOptionsFromConfig(config)); }, createDefaultHttpOptions(saveParamsOrConfig, config));
} }
public deleteDevice(deviceId: string, config?: RequestConfig) { public deleteDevice(deviceId: string, config?: RequestConfig) {

9
ui-ngx/src/app/core/http/entity-view.service.ts

@ -15,13 +15,14 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models';
import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models'; import { EntityView, EntityViewInfo, EntityViewSearchQuery } from '@app/shared/models/entity-view.models';
import { SaveEntityParams } from '@shared/models/entity.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -51,8 +52,10 @@ export class EntityViewService {
return this.http.get<EntityViewInfo>(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config)); return this.http.get<EntityViewInfo>(`/api/entityView/info/${entityViewId}`, defaultHttpOptionsFromConfig(config));
} }
public saveEntityView(entityView: EntityView, config?: RequestConfig): Observable<EntityView> { public saveEntityView(entityView: EntityView, config?: RequestConfig): Observable<EntityView>;
return this.http.post<EntityView>('/api/entityView', entityView, defaultHttpOptionsFromConfig(config)); public saveEntityView(entityView: EntityView, saveParams: SaveEntityParams, config?: RequestConfig): Observable<EntityView>;
public saveEntityView(entityView: EntityView, saveParamsOrConfig?: SaveEntityParams | RequestConfig, config?: RequestConfig): Observable<EntityView> {
return this.http.post<EntityView>('/api/entityView', entityView, createDefaultHttpOptions(saveParamsOrConfig, config));
} }
public deleteEntityView(entityViewId: string, config?: RequestConfig) { public deleteEntityView(entityViewId: string, config?: RequestConfig) {

34
ui-ngx/src/app/core/http/http-utils.ts

@ -18,32 +18,56 @@ import { InterceptorHttpParams } from '../interceptors/interceptor-http-params';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { InterceptorConfig } from '../interceptors/interceptor-config'; import { InterceptorConfig } from '../interceptors/interceptor-config';
export type QueryParams = { [param:string]: any };
export interface RequestConfig { export interface RequestConfig {
ignoreLoading?: boolean; ignoreLoading?: boolean;
ignoreErrors?: boolean; ignoreErrors?: boolean;
resendRequest?: boolean; resendRequest?: boolean;
queryParams?: QueryParams;
}
export function hasRequestConfig(config?: any): boolean {
if (!config) {
return false;
}
return config.hasOwnProperty('ignoreLoading') || config.hasOwnProperty('ignoreErrors') || config.hasOwnProperty('resendRequest') || config.hasOwnProperty('queryParams');
}
export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | RequestConfig, config?: RequestConfig) {
if (hasRequestConfig(queryParamsOrConfig)) {
return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig);
}
const queryParams = queryParamsOrConfig as QueryParams;
const finalConfig = {
...config,
...(queryParams && { queryParams }),
};
return defaultHttpOptionsFromConfig(finalConfig);
} }
export function defaultHttpOptionsFromConfig(config?: RequestConfig) { export function defaultHttpOptionsFromConfig(config?: RequestConfig) {
if (!config) { if (!config) {
config = {}; config = {};
} }
return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest); return defaultHttpOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest, config.queryParams);
} }
export function defaultHttpOptions(ignoreLoading: boolean = false, export function defaultHttpOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false, ignoreErrors: boolean = false,
resendRequest: boolean = false) { resendRequest: boolean = false,
queryParams?: QueryParams) {
return { return {
headers: new HttpHeaders({'Content-Type': 'application/json'}), headers: new HttpHeaders({'Content-Type': 'application/json'}),
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
}; };
} }
export function defaultHttpUploadOptions(ignoreLoading: boolean = false, export function defaultHttpUploadOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false, ignoreErrors: boolean = false,
resendRequest: boolean = false) { resendRequest: boolean = false,
queryParams?: QueryParams) {
return { return {
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest)) params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
}; };
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save