Browse Source

Merge remote-tracking branch 'upstream/rc' into bug/latest-values-map-with-key-value-agg

pull/14685/head
Ekaterina Chantsova 10 months ago
parent
commit
075378a1f5
  1. 2
      application/pom.xml
  2. 0
      application/src/main/data/resources/js_extensions/gateway-management-extension.js
  3. 132
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  4. 12
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  5. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java
  6. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java
  7. 24
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  8. 21
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  9. 5
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  10. 22
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  11. 23
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  12. 37
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  13. 20
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  14. 2
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  15. 48
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  16. 39
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  17. 19
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  18. 20
      application/src/main/java/org/thingsboard/server/controller/TenantController.java
  19. 30
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  20. 19
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  21. 50
      application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java
  22. 3
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
  23. 22
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  24. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  25. 24
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  26. 8
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  28. 7
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java
  29. 39
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  30. 4
      application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java
  31. 12
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  32. 101
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  33. 15
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  34. 36
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java
  35. 15
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java
  36. 32
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  37. 40
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java
  38. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java
  39. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  40. 29
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  41. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java
  42. 26
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java
  43. 64
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java
  44. 80
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java
  45. 3
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  46. 1
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  47. 167
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  48. 17
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  49. 13
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  50. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java
  51. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  52. 48
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java
  53. 55
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java
  54. 10
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/BaseAiModelProcessor.java
  55. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  56. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java
  57. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java
  58. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  59. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  60. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java
  61. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java
  62. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java
  63. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  64. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  65. 13
      application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java
  66. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  67. 5
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  68. 13
      application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java
  69. 2
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  70. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  71. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  72. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  73. 4
      application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java
  74. 14
      application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java
  75. 5
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  76. 2
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  77. 2
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  78. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  79. 18
      application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
  80. 4
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  81. 37
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java
  82. 2
      application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java
  83. 68
      application/src/main/resources/thingsboard.yml
  84. 181
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  85. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  86. 2
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  87. 38
      application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java
  88. 2
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  89. 41
      application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
  90. 41
      application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
  91. 2
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  92. 42
      application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java
  93. 2
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  94. 35
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  95. 2
      application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java
  96. 45
      application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
  97. 2
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  98. 38
      application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
  99. 41
      application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
  100. 37
      application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.0-RC</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>

0
application/src/main/data/resources/js_modules/gateway-management-extension.js → application/src/main/data/resources/js_extensions/gateway-management-extension.js

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

@ -36,10 +36,12 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -52,10 +54,13 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState;
import java.util.ArrayList;
import java.util.Collection;
@ -71,6 +76,8 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
/**
@ -128,6 +135,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else {
removeState(cfId);
}
msg.getCallback().onSuccess();
}
public void process(CalculatedFieldStatePartitionRestoreMsg msg) {
@ -159,10 +167,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else {
state.setCtx(ctx, actorCtx);
}
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
if (msg.getStateAction() != StateAction.REFRESH_CTX) {
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
msg.getCallback().onSuccess();
}
} catch (Exception e) {
log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e);
@ -222,10 +234,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void handleRelationUpdate(CalculatedFieldRelationActionMsg msg) throws CalculatedFieldException {
CalculatedFieldCtx ctx = msg.getCalculatedField();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback());
var state = states.get(ctx.getCfId());
try {
Map<String, ArgumentEntry> updatedArgs = new HashMap<>();
Map<String, ArgumentEntry> updatedArgs = null;
if (state == null) {
state = createState(ctx);
} else {
@ -233,11 +244,19 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, msg.getRelatedEntityId(), ctx.getArguments());
updatedArgs = relatedEntitiesAggState.updateEntityData(setEntityIdToSingleEntityArguments(msg.getRelatedEntityId(), fetchedArgs));
}
if (state instanceof PropagationCalculatedFieldState propagationState) {
PropagationArgumentEntry entry = new PropagationArgumentEntry();
entry.setAdded(List.of(msg.getRelatedEntityId()));
updatedArgs = propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx);
}
if (CollectionsUtil.isEmpty(updatedArgs)) {
msg.getCallback().onSuccess();
return;
}
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
}
if (state.isSizeOk()) {
processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, callback);
processStateIfReady(state, updatedArgs, ctx, Collections.singletonList(ctx.getCfId()), null, null, msg.getCallback());
} else {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
@ -268,9 +287,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
}
} else {
msg.getCallback().onSuccess();
return;
}
if (state instanceof PropagationCalculatedFieldState propagationState) {
PropagationArgumentEntry entry = new PropagationArgumentEntry();
entry.setRemoved(msg.getRelatedEntityId());
propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx);
}
msg.getCallback().onSuccess();
}
public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException {
@ -401,19 +425,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) {
state = createState(ctx);
justRestored = true;
} else if (ctx.shouldFetchRelationQueryDynamicArgumentsFromDb(state)) {
log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
try {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
if (ctx.getCfType() == CalculatedFieldType.GEOFENCING) {
var geofencingState = (GeofencingCalculatedFieldState) state;
geofencingState.updateLastDynamicArgumentsRefreshTs();
}
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
} else if (ctx.shouldFetchEntityRelations(state)) {
} else if (ctx.shouldFetchRelatedEntities(state)) {
log.debug("[{}][{}] Going to update related entities for CF.", entityId, ctx.getCfId());
try {
if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesState) {
@ -427,6 +439,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
justRestored = true;
}
}
if (state instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
geofencingCalculatedFieldState.updateScheduledRefreshTs();
}
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
@ -456,9 +473,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
state.setCtx(ctx, actorCtx);
state.init(false);
if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isRelationQueryDynamicArguments()) {
if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) {
GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state;
geofencingState.updateLastDynamicArgumentsRefreshTs();
geofencingState.updateScheduledRefreshTs();
}
Map<String, ArgumentEntry> arguments = fetchArguments(ctx);
@ -567,30 +584,61 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) {
for (TsKvProto item : data) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null);
Set<String> argNames = relatedEntityArgs.get(key);
if (argNames != null) {
argNames.forEach(argName -> {
arguments.put(argName, new SingleValueArgumentEntry(originator, item));
});
}
argNames = args.get(key);
if (argNames != null) {
argNames.forEach(argName -> {
arguments.put(argName, new SingleValueArgumentEntry(item));
});
}
SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item);
mapLatest(relatedArgIncoming, relatedEntityArgs.get(key), arguments);
SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item);
mapLatest(incoming, args.get(key), arguments);
key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null);
argNames = args.get(key);
if (argNames != null) {
argNames.forEach(argName -> {
arguments.put(argName, new SingleValueArgumentEntry(item));
});
}
mapRolling(item, args.get(key), arguments);
}
}
return arguments;
}
private void mapLatest(SingleValueArgumentEntry incoming,
Set<String> argNames,
Map<String, ArgumentEntry> arguments) {
if (argNames != null) {
argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> {
if (existing == null) {
return incoming;
}
existing.updateEntry(incoming);
return existing;
}));
}
}
private void mapRolling(TsKvProto item,
Set<String> argNames,
Map<String, ArgumentEntry> arguments) {
if (argNames != null) {
Double recordValue = getValueForTsRecord(ProtoUtils.fromProto(item.getKv()));
argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> {
if (existing instanceof TsRollingArgumentEntry rolling) {
if (recordValue != null) {
rolling.getTsRecords().put(item.getTs(), recordValue);
}
return rolling;
}
TsRollingArgumentEntry rolling = new TsRollingArgumentEntry();
if (recordValue != null) {
rolling.getTsRecords().put(item.getTs(), recordValue);
}
if (existing instanceof SingleValueArgumentEntry single) {
Double existingValue = getValueForTsRecord(single.getKvEntryValue());
if (existingValue != null) {
rolling.getTsRecords().put(single.getTs(), existingValue);
}
}
return rolling;
}));
}
}
private Map<String, ArgumentEntry> mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, attrDataList);
}

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

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.HasRelationPathLevel;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -166,10 +167,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
if (ctx != null) {
msg.setCtx(ctx);
log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId());
log.debug("[{}] Pushing CF state restore msg to specific actor [{}]", tenantId, msg.getId().entityId());
getOrCreateActor(msg.getId().entityId()).tellWithHighPriority(msg);
} else {
} else if (msg.getState() != null) {
log.debug("[{}] Received CF state restore msg for non-existing CF [{}]. Removing state", tenantId, cfId);
cfStateService.deleteState(msg.getId(), msg.getCallback());
} else {
msg.getCallback().onSuccess();
}
}
@ -370,7 +374,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
List<CalculatedFieldCtx> matchingCfs = cfsByEntityIdAndProfile.stream()
.filter(cf -> {
if (cf.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration config) {
if (cf.getCalculatedField().getConfiguration() instanceof HasRelationPathLevel config) {
RelationPathLevel relation = config.getRelation();
return direction.equals(relation.direction()) && relationType.equals(relation.relationType());
}
@ -462,6 +466,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
stateAction = StateAction.REINIT; // refetch arguments, call state.init, then calculate
} else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) {
stateAction = StateAction.REPROCESS; // call state.setCtx, then calculate
} else if (newCfCtx.hasRefreshContextOnlyChanges(oldCfCtx)) {
stateAction = StateAction.REFRESH_CTX;
} else {
callback.onSuccess();
return;

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java

@ -19,6 +19,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
@ -30,6 +31,7 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs
private final CalculatedFieldEntityCtxId id;
private final CalculatedFieldState state;
private final TopicPartitionInfo partition;
private final TbCallback callback;
private CalculatedFieldCtx ctx;
@Override
@ -41,4 +43,5 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs
public TenantId getTenantId() {
return id.tenantId();
}
}

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java

@ -39,6 +39,7 @@ public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg
INIT,
REINIT,
RECREATE,
REPROCESS
REPROCESS,
REFRESH_CTX
}
}

24
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.TbMsg;
@ -139,13 +138,22 @@ public class TenantActor extends RuleChainManagerActor {
@Override
protected boolean doProcess(TbActorMsg msg) {
if (cantFindTenant) {
log.info("[{}] Processing missing Tenant msg: {}", tenantId, msg);
if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) {
QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg;
queueMsg.getMsg().getCallback().onSuccess();
} else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) {
TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg;
transportMsg.getCallback().onSuccess();
log.debug("[{}] Processing message for non-existing tenant: {}", tenantId, msg);
switch (msg.getMsgType()) {
case QUEUE_TO_RULE_ENGINE_MSG -> {
((QueueToRuleEngineMsg) msg).getMsg().getCallback().onSuccess();
}
case TRANSPORT_TO_DEVICE_ACTOR_MSG -> {
((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess();
}
case CF_STATE_RESTORE_MSG -> {
((CalculatedFieldStateRestoreMsg) msg).getCallback().onSuccess();
}
default -> {
if (!log.isDebugEnabled()) {
log.info("[{}] Processing message for non-existing tenant: {}", tenantId, msg);
}
}
}
return true;
}

21
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -16,11 +16,13 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -45,7 +47,10 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
@ -222,4 +227,20 @@ public class AssetProfileController extends BaseController {
return checkNotNull(assetProfileService.findAssetProfileNamesByTenantId(tenantId, activeOnly));
}
@ApiOperation(value = "Get Asset Profiles By Ids (getAssetProfilesByIds)",
notes = "Requested asset profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/assetProfileInfos", params = {"assetProfileIds"})
public List<AssetProfileInfo> getAssetProfilesByIds(
@Parameter(description = "A list of asset profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("assetProfileIds") Set<UUID> assetProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<AssetProfileId> assetProfileIds = new ArrayList<>();
for (UUID assetProfileUUID : assetProfileUUIDs) {
assetProfileIds.add(new AssetProfileId(assetProfileUUID));
}
return assetProfileService.findAssetProfilesByIds(tenantId, assetProfileIds);
}
}

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

@ -150,7 +150,6 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
@ -174,6 +173,8 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.exception.EntitiesLimitExceededException;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
@ -452,6 +453,8 @@ public abstract class BaseController {
}
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} else if (exception instanceof EntitiesLimitExceededException) {
return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED);
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);

22
application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java

@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
@ -68,7 +69,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@ -198,12 +199,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Calculated field type filter.")
@RequestParam CalculatedFieldType type,
@Parameter(description = "Calculated field types filter.")
@RequestParam(required = false) Set<CalculatedFieldType> types,
@Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.")
@RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.")
@RequestParam(required = false) List<UUID> entities,
@RequestParam(required = false) Set<UUID> entities,
@Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2")
@RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma)
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@ -215,6 +216,12 @@ public class CalculatedFieldController extends BaseController {
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
if (CollectionUtils.isEmpty(types)) {
types = EnumSet.allOf(CalculatedFieldType.class);
types.remove(CalculatedFieldType.ALARM);
}
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet();
@ -223,10 +230,10 @@ public class CalculatedFieldController extends BaseController {
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.type(type)
.types(types)
.entityTypes(entityTypes)
.entityIds(entities)
.names(params.get("name"))
.names(Optional.ofNullable(params.get("name")).map(HashSet::new).orElse(null))
.build();
return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink);
}
@ -361,8 +368,7 @@ public class CalculatedFieldController extends BaseController {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default ->
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}
}
}

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

@ -18,10 +18,12 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -46,6 +48,11 @@ import org.thingsboard.server.service.entitiy.customer.TbCustomerService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
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_TEXT_SEARCH_DESCRIPTION;
@ -195,4 +202,20 @@ public class CustomerController extends BaseController {
return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found");
}
@ApiOperation(value = "Get customers by Customer Ids (getCustomersByIds)",
notes = "Returns a list of Customer objects based on the provided ids." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/customers", params = {"customerIds"})
public List<Customer> getCustomersByIds(
@Parameter(description = "A list of customer ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("customerIds") Set<UUID> customerUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<CustomerId> customerIds = new ArrayList<>();
for (UUID customerUUID : customerUUIDs) {
customerIds.add(new CustomerId(customerUUID));
}
return customerService.findCustomersByTenantIdAndIds(tenantId, customerIds);
}
}

37
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
@ -62,6 +63,7 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -592,14 +594,7 @@ public class DashboardController extends BaseController {
checkEdgeId(edgeId, Operation.READ);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageData<DashboardInfo> nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
List<DashboardInfo> filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboardInfo.getId(), dashboardInfo);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
List<DashboardInfo> filteredDashboards = filterDashboardsByReadPermission(nonFilteredResult.getData());
PageData<DashboardInfo> filteredResult = new PageData<>(filteredDashboards,
nonFilteredResult.getTotalPages(),
nonFilteredResult.getTotalElements(),
@ -607,6 +602,22 @@ public class DashboardController extends BaseController {
return checkNotNull(filteredResult);
}
@ApiOperation(value = "Get dashboards by Dashboard Ids (getDashboardsByIds)",
notes = "Returns a list of DashboardInfo objects based on the provided ids. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboards", params = {"dashboardIds"})
public List<DashboardInfo> getDashboardsByIds(@Parameter(description = "A list of dashboard ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("dashboardIds") Set<UUID> dashboardUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<DashboardId> dashboardIds = new ArrayList<>();
for (UUID dashboardUUID : dashboardUUIDs) {
dashboardIds.add(new DashboardId(dashboardUUID));
}
List<DashboardInfo> dashboards = dashboardService.findDashboardInfoByIds(tenantId, dashboardIds);
return filterDashboardsByReadPermission(dashboards);
}
private Set<CustomerId> customerIdFromStr(String[] strCustomerIds) {
Set<CustomerId> customerIds = new HashSet<>();
if (strCustomerIds != null) {
@ -617,4 +628,14 @@ public class DashboardController extends BaseController {
return customerIds;
}
private List<DashboardInfo> filterDashboardsByReadPermission(List<DashboardInfo> dashboards) {
return dashboards.stream().filter(dashboard -> {
try {
return accessControlService.hasPermission(getCurrentUser(), Resource.DASHBOARD, Operation.READ, dashboard.getId(), dashboard);
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
}
}

20
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -16,12 +16,14 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -48,7 +50,9 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA;
@ -278,4 +282,20 @@ public class DeviceProfileController extends BaseController {
return checkNotNull(deviceProfileService.findDeviceProfileNamesByTenantId(tenantId, activeOnly));
}
@ApiOperation(value = "Get Device Profile Infos By Ids (getDeviceProfilesByIds)",
notes = "Requested device profiles must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/deviceProfileInfos", params = {"deviceProfileIds"})
public List<DeviceProfileInfo> getDeviceProfileInfosByIds(
@Parameter(description = "A list of device profile ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("deviceProfileIds") Set<UUID> deviceProfileUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<DeviceProfileId> deviceProfileIds = new ArrayList<>();
for (UUID deviceProfileUUID : deviceProfileUUIDs) {
deviceProfileIds.add(new DeviceProfileId(deviceProfileUUID));
}
return deviceProfileService.findDeviceProfilesByIds(tenantId, deviceProfileIds);
}
}

2
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -54,7 +54,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportReques
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;

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

@ -17,11 +17,13 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -57,7 +59,10 @@ import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@ -346,14 +351,7 @@ public class EntityViewController extends BaseController {
checkNotNull(query.getEntityViewTypes());
checkEntityId(query.getParameters().getEntityId(), Operation.READ);
List<EntityView> entityViews = checkNotNull(entityViewService.findEntityViewsByQuery(getTenantId(), query).get());
entityViews = entityViews.stream().filter(entityView -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
entityViews = filterEntityViewsByReadPermission(entityViews);
return entityViews;
}
@ -453,14 +451,7 @@ public class EntityViewController extends BaseController {
} else {
nonFilteredResult = entityViewService.findEntityViewsByTenantIdAndEdgeId(tenantId, edgeId, pageLink);
}
List<EntityView> filteredEntityViews = nonFilteredResult.getData().stream().filter(entityView -> {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView);
return true;
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
List<EntityView> filteredEntityViews = filterEntityViewsByReadPermission(nonFilteredResult.getData());
PageData<EntityView> filteredResult = new PageData<>(filteredEntityViews,
nonFilteredResult.getTotalPages(),
nonFilteredResult.getTotalElements(),
@ -468,4 +459,29 @@ public class EntityViewController extends BaseController {
return checkNotNull(filteredResult);
}
@ApiOperation(value = "Get Entity Views By Ids (getEntityViewsByIds)",
notes = "Requested entity views must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/entityViews", params = {"entityViewIds"})
public List<EntityView> getEntityViewsByIds(@Parameter(description = "A list of entity view ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("entityViewIds") Set<UUID> entityViewUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<EntityViewId> entityViewIds = new ArrayList<>();
for (UUID entityViewUUID : entityViewUUIDs) {
entityViewIds.add(new EntityViewId(entityViewUUID));
}
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndIds(tenantId, entityViewIds);
return filterEntityViewsByReadPermission(entityViews);
}
private List<EntityView> filterEntityViewsByReadPermission(List<EntityView> entityViews) {
return entityViews.stream().filter(entityView -> {
try {
return accessControlService.hasPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.READ, entityView.getId(), entityView);
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
}
}

39
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -35,6 +36,8 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
@ -44,6 +47,9 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntitiesLimitIncreaseRequestNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
@ -51,6 +57,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
@ -69,6 +76,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.Comparator;
import java.util.HashMap;
@ -76,6 +84,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -90,6 +99,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@ -105,6 +115,7 @@ public class NotificationController extends BaseController {
private final NotificationTargetService notificationTargetService;
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
private final SystemSecurityService systemSecurityService;
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
@ -282,6 +293,34 @@ public class NotificationController extends BaseController {
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null));
}
@ApiOperation(value = "Send entity limit increase request notification to System administrators (sendEntitiesLimitIncreaseRequest)",
notes = "Send entity limit increase request notification by Tenant Administrator to System administrators." +
TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/notification/entitiesLimitIncreaseRequest/{entityType}")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void sendEntitiesLimitIncreaseRequest(@Parameter(description = "Entity type", required = true, schema = @Schema(allowableValues = {"DEVICE", "ASSET", "CUSTOMER", "USER", "DASHBOARD", "RULE_CHAIN", "EDGE"}))
@PathVariable("entityType") String strEntityType,
@AuthenticationPrincipal SecurityUser user,
HttpServletRequest request) throws Exception {
EntityType entityType = checkEnumParameter("entityType", strEntityType, EntityType::valueOf);
Optional<NotificationTarget> sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(TenantId.SYS_TENANT_ID, UsersFilterType.SYSTEM_ADMINISTRATORS)
.stream().findFirst();
if (sysAdmins.isPresent()) {
NotificationTargetId notificationTargetId = sysAdmins.get().getId();
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
NotificationInfo info = EntitiesLimitIncreaseRequestNotificationInfo.builder()
.entityType(entityType)
.userEmail(user.getEmail())
.increaseLimitActionLabel("Set new limit")
.increaseLimitLink("/tenants/"+user.getTenantId().toString())
.baseUrl(baseUrl)
.build();
notificationCenter.sendSystemNotification(TenantId.SYS_TENANT_ID, notificationTargetId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, info);
} else {
throw new IllegalArgumentException("Notification target for 'System administrators' not found");
}
}
@ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)",
notes = "Returns preview for notification request." + NEW_LINE +
"`processedTemplates` shows how the notifications for each delivery method will look like " +

19
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
@ -79,6 +80,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@ -90,6 +92,7 @@ import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIG
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
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_SIZE_DESCRIPTION;
@ -579,4 +582,20 @@ public class RuleChainController extends BaseController {
return checkNotNull(result);
}
@ApiOperation(value = "Get Rule Chains By Ids (getRuleChainsByIds)",
notes = "Requested rule chains must be owned by tenant which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/ruleChains", params = {"ruleChainIds"})
public List<RuleChain> getRuleChainsByIds(
@Parameter(description = "A list of rule chain ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("ruleChainIds") Set<UUID> ruleChainUUIDs) throws Exception {
TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainId> ruleChainIds = new ArrayList<>();
for (UUID ruleChainUUID : ruleChainUUIDs) {
ruleChainIds.add(new RuleChainId(ruleChainUUID));
}
return ruleChainService.findRuleChainsByIds(tenantId, ruleChainIds);
}
}

20
application/src/main/java/org/thingsboard/server/controller/TenantController.java

@ -16,11 +16,13 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -43,6 +45,11 @@ import org.thingsboard.server.service.entitiy.tenant.TbTenantService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
@ -162,4 +169,17 @@ public class TenantController extends BaseController {
return checkNotNull(tenantService.findTenantInfos(pageLink));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@GetMapping(value = "/tenants", params = {"tenantIds"})
public List<Tenant> getTenantsByIds(
@Parameter(description = "A list of tenant ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("tenantIds") Set<UUID> tenantUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<TenantId> tenantIds = new ArrayList<>();
for (UUID tenantIdUUID : tenantUUIDs) {
tenantIds.add(TenantId.fromUUID(tenantIdUUID));
}
return tenantService.findTenantsByIds(tenantId, tenantIds);
}
}

30
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@ -82,6 +83,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIELD;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
@ -579,6 +583,32 @@ public class UserController extends BaseController {
userService.removeMobileSession(user.getTenantId(), mobileToken);
}
@ApiOperation(value = "Get Users By Ids (getUsersByIds)",
notes = "Requested users must be owned by tenant or assigned to customer which user is performing the request. ")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/users", params = {"userIds"})
public List<User> getUsersByIds(
@Parameter(description = "A list of user ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("userIds") Set<UUID> userUUIDs) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<UserId> userIds = new ArrayList<>();
for (UUID userUUID : userUUIDs) {
userIds.add(new UserId(userUUID));
}
List<User> users = userService.findUsersByTenantIdAndIds(tenantId, userIds);
return filterUsersByReadPermission(users);
}
private List<User> filterUsersByReadPermission(List<User> users) {
return users.stream().filter(user -> {
try {
return accessControlService.hasPermission(getCurrentUser(), Resource.USER, Operation.READ, user.getId(), user);
} catch (ThingsboardException e) {
return false;
}
}).collect(Collectors.toList());
}
private void checkNotReserved(String strType, UserSettingsType type) throws ThingsboardException {
if (type.isReserved()) {
throw new ThingsboardException("Settings with type: " + strType + " are reserved for internal use!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);

19
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -16,10 +16,12 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -49,10 +51,12 @@ import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES;
import static org.thingsboard.server.controller.ControllerConstants.INLINE_IMAGES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
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_SIZE_DESCRIPTION;
@ -231,4 +235,19 @@ public class WidgetsBundleController extends BaseController {
}
}
@ApiOperation(value = "Get Widgets Bundles By Ids (getWidgetsBundlesByIds)",
notes = "Requested widgets bundles must be system level or owned by tenant of the user which is performing the request. " +
NEW_LINE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/widgetsBundles", params = {"widgetsBundleIds"})
public List<WidgetsBundle> getWidgetsBundlesByIds(
@Parameter(description = "A list of widgets bundle ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")), required = true)
@RequestParam("widgetsBundleIds") Set<UUID> widgetsBundleUUIDs) throws ThingsboardException {
List<WidgetsBundleId> widgetsBundleIds = new ArrayList<>();
for (UUID widgetsBundleUUID : widgetsBundleUUIDs) {
widgetsBundleIds.add(new WidgetsBundleId(widgetsBundleUUID));
}
return widgetsBundleService.findSystemOrTenantWidgetsBundlesByIds(getTenantId(), widgetsBundleIds);
}
}

50
application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java

@ -0,0 +1,50 @@
/**
* 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.exception;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@Schema
public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse {
private final EntityType entityType;
private final Long limit;
protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) {
super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN);
this.entityType = entityType;
this.limit = limit;
}
public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) {
return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit);
}
@Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY)
public EntityType getEntityType() {
return entityType;
}
@Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY)
public Long getLimit() {
return limit;
}
}

3
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java

@ -64,7 +64,8 @@ public class ThingsboardErrorResponse {
"\n\n* `32` - Item not found (HTTP: 404 - Not Found)" +
"\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" +
"\n\n* `34` - Too many updates (Too many updates over Websocket session)" +
"\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)",
"\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)" +
"\n\n* `41` - Entities limit exceeded (HTTP: 403 - Forbidden)",
example = "10", type = "integer",
accessMode = Schema.AccessMode.READ_ONLY)
public ThingsboardErrorCode getErrorCode() {

22
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -47,6 +47,7 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
@ -95,6 +96,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN);
errorCodeToStatusMap.put(ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN);
errorCodeToStatusMap.put(ThingsboardErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT);
}
@ -143,6 +145,12 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleSubscriptionException(thingsboardException, response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) {
handleDatabaseException(thingsboardException.getCause(), response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) {
if (thingsboardException.getCause() instanceof EntitiesLimitExceededException entitiesLimitExceededException) {
handleEntitiesLimitExceededException(entitiesLimitExceededException, response);
} else {
handleEntitiesLimitExceededException(thingsboardException, response);
}
} else {
handleThingsboardException(thingsboardException, response);
}
@ -218,6 +226,20 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
writeResponse(errorResponse, response);
}
private void handleEntitiesLimitExceededException(ThingsboardException entitiesLimitExceededException, HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitExceededException.getCause()).getResponseBodyAsByteArray(), Object.class));
}
private void handleEntitiesLimitExceededException(EntitiesLimitExceededException entitiesLimitExceededException, HttpServletResponse response) throws IOException {
EntityType entityType = entitiesLimitExceededException.getEntityType();
Long limit = entitiesLimitExceededException.getLimit();
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
ThingsboardEntitiesLimitExceededResponse.of(entitiesLimitExceededException.getMessage(), entityType, limit));
}
private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -116,6 +116,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();

24
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -22,17 +22,18 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy;
import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.AttributeScope;
@ -80,7 +81,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
@ -391,6 +391,24 @@ public abstract class AbstractCalculatedFieldProcessingService {
return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE);
}
protected void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback,
TriConsumer<EntityId, TelemetryCalculatedFieldResult, TbCallback> telemetryResultHandler) {
List<EntityId> propagationEntityIds = propagationResult.getEntityIds();
if (propagationEntityIds.isEmpty()) {
callback.onSuccess();
return;
}
if (propagationEntityIds.size() == 1) {
EntityId propagationEntityId = propagationEntityIds.get(0);
telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback);
return;
}
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback);
for (var propagationEntityId : propagationEntityIds) {
telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback);
}
}
protected void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) {
try {
clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() {
@ -413,7 +431,7 @@ public abstract class AbstractCalculatedFieldProcessingService {
protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, String cfName, TelemetryCalculatedFieldResult cfResult, List<CalculatedFieldId> cfIds, TbCallback callback) {
OutputType type = cfResult.getType();
JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue()));
JsonElement jsonResult = cfResult.toJsonElement();
log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult);
switch (type) {

8
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java

@ -68,7 +68,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
protected abstract void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback);
protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition) {
protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition, TbCallback callback) {
var id = fromProto(stateMsg.getId());
if (partition == null) {
try {
@ -79,12 +79,12 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
}
}
var state = fromProto(id, stateMsg);
processRestoredState(id, state, partition);
processRestoredState(id, state, partition, callback);
}
protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition) {
protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition, TbCallback callback) {
partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME);
actorSystemContext.tellWithHighPriority(new CalculatedFieldStateRestoreMsg(id, state, partition));
actorSystemContext.tellWithHighPriority(new CalculatedFieldStateRestoreMsg(id, state, partition, callback));
}
@Override

4
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java

@ -27,9 +27,11 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface CalculatedFieldProcessingService {
@ -37,6 +39,8 @@ public interface CalculatedFieldProcessingService {
Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId);
Optional<PropagationArgumentEntry> fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId);
List<EntityId> fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);

7
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java

@ -15,11 +15,14 @@
*/
package org.thingsboard.server.service.cf;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
import java.util.Objects;
public interface CalculatedFieldResult {
@ -29,4 +32,8 @@ public interface CalculatedFieldResult {
boolean isEmpty();
default JsonElement toJsonElement() {
return JsonParser.parseString(Objects.requireNonNull(stringValue()));
}
}

39
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -17,13 +17,13 @@ package org.thingsboard.server.service.cf;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.function.TriConsumer;
import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.MultipleTbCallback;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric;
import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration;
@ -50,6 +50,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.ArrayList;
@ -57,6 +58,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
@ -94,11 +96,18 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
@Override
public Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
return switch (ctx.getCfType()) {
case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis()));
case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId)));
default -> Collections.emptyMap();
};
return ctx.getCfType() == CalculatedFieldType.GEOFENCING ?
resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())) :
Collections.emptyMap();
}
@Override
public Optional<PropagationArgumentEntry> fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
if (ctx.getCfType() != CalculatedFieldType.PROPAGATION) {
return Optional.empty();
}
return Optional.of((PropagationArgumentEntry)
resolveArgumentValue(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId)));
}
@Override
@ -169,24 +178,6 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds));
}
private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback,
TriConsumer<EntityId, TelemetryCalculatedFieldResult, TbCallback> telemetryResultHandler) {
List<EntityId> propagationEntityIds = propagationResult.getPropagationEntityIds();
if (propagationEntityIds.isEmpty()) {
callback.onSuccess();
return;
}
if (propagationEntityIds.size() == 1) {
EntityId propagationEntityId = propagationEntityIds.get(0);
telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback);
return;
}
MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback);
for (var propagationEntityId : propagationEntityIds) {
telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback);
}
}
@Override
public void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback) {
Map<TopicPartitionInfo, List<CalculatedFieldEntityCtxId>> unicasts = new HashMap<>();

4
application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java

@ -28,7 +28,7 @@ import java.util.List;
@Builder
public final class PropagationCalculatedFieldResult implements CalculatedFieldResult {
private final List<EntityId> propagationEntityIds;
private final List<EntityId> entityIds;
private final TelemetryCalculatedFieldResult result;
@Override
@ -43,7 +43,7 @@ public final class PropagationCalculatedFieldResult implements CalculatedFieldRe
@Override
public boolean isEmpty() {
return CollectionsUtil.isEmpty(propagationEntityIds) || result.isEmpty();
return CollectionsUtil.isEmpty(entityIds) || result.isEmpty();
}
}

12
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -48,7 +48,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
protected Map<String, ArgumentEntry> arguments = new HashMap<>();
protected boolean sizeExceedsLimit;
protected long latestTimestamp = DEFAULT_LAST_UPDATE_TS;
protected ReadinessStatus readinessStatus;
@Setter
@ -63,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
this.ctx = ctx;
this.actorCtx = actorCtx;
this.requiredArguments = ctx.getArgNames();
this.readinessStatus = checkReadiness(requiredArguments, arguments);
this.readinessStatus = checkReadiness();
}
@Override
@ -108,7 +107,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
if (updatedArguments == null) {
return Collections.emptyMap();
}
readinessStatus = checkReadiness(requiredArguments, arguments);
readinessStatus = checkReadiness();
return updatedArguments;
}
@ -121,7 +120,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
requiredArguments = null;
arguments.clear();
sizeExceedsLimit = false;
latestTimestamp = DEFAULT_LAST_UPDATE_TS;
}
@Override
@ -183,13 +181,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
return latestTs;
}
protected ReadinessStatus checkReadiness(List<String> requiredArguments, Map<String, ArgumentEntry> currentArguments) {
if (currentArguments == null) {
protected ReadinessStatus checkReadiness() {
if (arguments == null) {
return ReadinessStatus.from(requiredArguments);
}
List<String> emptyArguments = null;
for (String requiredArgumentKey : requiredArguments) {
ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey);
ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey);
if (argumentEntry == null || argumentEntry.isEmpty()) {
if (emptyArguments == null) {
emptyArguments = new ArrayList<>();

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

@ -63,8 +63,7 @@ import org.thingsboard.server.dao.util.TimeUtils;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.io.Closeable;
@ -122,14 +121,14 @@ public class CalculatedFieldCtx implements Closeable {
private long maxSingleValueArgumentSize;
private long intermediateAggregationIntervalMillis;
private boolean relationQueryDynamicArguments;
private boolean cfHasRelationPathQuerySource;
private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityAndCurrentOwnerGeofencingArgumentNames;
private List<String> relatedEntityArgumentNames;
private long scheduledUpdateIntervalMillis;
private long cfCheckReevaluationInterval;
private long alarmReevaluationInterval;
private long cfCheckReevaluationIntervalMillis;
private long alarmReevaluationIntervalMillis;
private Argument propagationArgument;
private boolean applyExpressionForResolvedArguments;
@ -161,10 +160,11 @@ public class CalculatedFieldCtx implements Closeable {
if (refId == null) {
if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cfType)) {
relatedEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey()));
cfHasRelationPathQuerySource = true;
continue;
}
if (entry.getValue().hasRelationQuerySource()) {
relationQueryDynamicArguments = true;
cfHasRelationPathQuerySource = true;
continue;
}
if (entry.getValue().hasOwnerSource()) {
@ -201,7 +201,7 @@ public class CalculatedFieldCtx implements Closeable {
if (calculatedField.getConfiguration() instanceof PropagationCalculatedFieldConfiguration propagationConfig) {
propagationArgument = propagationConfig.toPropagationArgument();
applyExpressionForResolvedArguments = propagationConfig.isApplyExpressionToResolvedArguments();
relationQueryDynamicArguments = true;
cfHasRelationPathQuerySource = true;
}
}
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
@ -246,8 +246,7 @@ public class CalculatedFieldCtx implements Closeable {
boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) {
if (requiresScheduledReevaluation) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(alarmReevaluationInterval);
if (now - lastReevaluationTs >= reevaluationIntervalMillis) {
if (now - lastReevaluationTs >= alarmReevaluationIntervalMillis) {
lastReevaluationTs = now;
return true;
}
@ -306,8 +305,8 @@ public class CalculatedFieldCtx implements Closeable {
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getIntermediateAggregationIntervalInSecForCF));
this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval);
this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval);
this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval));
this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval));
}
public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) {
@ -636,16 +635,35 @@ public class CalculatedFieldCtx implements Closeable {
return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId);
}
public boolean hasRefreshContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state recalculation
if (output != null) {
var thisOutputStrategy = output.getStrategy();
var otherOutputStrategy = other.getCalculatedField().getConfiguration().getOutput().getStrategy();
if (thisOutputStrategy.hasRefreshContextOnlyChanges(otherOutputStrategy)) {
return true;
}
}
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
if (thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult()) {
return true;
}
}
return false;
}
public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly
if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !Objects.equals(expression, other.expression)) {
return true;
}
if (!Objects.equals(output, other.output)) {
if (output != null && output.hasContextOnlyChanges(other.output)) {
return true;
}
if (calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig
&& thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) {
&& other.calculatedField.getConfiguration() instanceof SimpleCalculatedFieldConfiguration otherConfig
&& thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs()) {
return true;
}
if (cfType == CalculatedFieldType.ALARM) {
@ -667,18 +685,19 @@ public class CalculatedFieldCtx implements Closeable {
return true;
}
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
&& other.getCalculatedField().getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig
&& (thisConfig.getDeduplicationIntervalInSec() != otherConfig.getDeduplicationIntervalInSec()
|| !thisConfig.getMetrics().equals(otherConfig.getMetrics())
|| thisConfig.isUseLatestTs() != otherConfig.isUseLatestTs())) {
return true;
}
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
&& other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics());
boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark());
boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult();
return metricsChanged || watermarkChanged || produceIntermediateResultChanged;
if (metricsChanged || watermarkChanged) {
return true;
}
}
return false;
}
@ -711,7 +730,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
}
return false;
@ -719,7 +738,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasRelatedEntitiesAggregationConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getRelation().equals(otherConfig.getRelation());
}
return false;
@ -727,7 +746,7 @@ public class CalculatedFieldCtx implements Closeable {
private boolean hasEntityAggregationConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) {
return !thisConfig.getInterval().equals(otherConfig.getInterval());
}
return false;
@ -737,38 +756,20 @@ public class CalculatedFieldCtx implements Closeable {
return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE;
}
public boolean shouldFetchRelationQueryDynamicArgumentsFromDb(CalculatedFieldState state) {
if (!relationQueryDynamicArguments) {
public boolean shouldFetchRelatedEntities(CalculatedFieldState state) {
if (!cfHasRelationPathQuerySource) {
return false;
}
return switch (cfType) {
case PROPAGATION -> true;
case GEOFENCING -> {
if (isScheduledUpdateDisabled()) {
yield false;
}
var geofencingState = (GeofencingCalculatedFieldState) state;
if (geofencingState.getLastDynamicArgumentsRefreshTs() == DEFAULT_LAST_UPDATE_TS) {
yield true;
}
yield geofencingState.getLastDynamicArgumentsRefreshTs() <
System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
default -> false;
};
}
public boolean shouldFetchEntityRelations(CalculatedFieldState state) {
if (!(state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState)) {
if (isScheduledUpdateDisabled()) {
return false;
}
if (isScheduledUpdateDisabled()) {
if (!(state instanceof ScheduledRefreshSupported scheduledRefreshSupported)) {
return false;
}
if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == DEFAULT_LAST_UPDATE_TS) {
if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) {
return true;
}
return relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
@Override
@ -796,10 +797,10 @@ public class CalculatedFieldCtx implements Closeable {
@Override
public String toString() {
return "CalculatedFieldCtx{" +
"cfId=" + cfId +
", cfType=" + cfType +
", entityId=" + entityId +
'}';
"cfId=" + cfId +
", cfType=" + cfType +
", entityId=" + entityId +
'}';
}
}

15
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -110,7 +110,13 @@ public interface CalculatedFieldState extends Closeable {
private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " +
"Verify the configured relation type and direction.";
private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: ";
private static final ReadinessStatus READY = new ReadinessStatus(true, null);
public static final String MISSING_AGGREGATION_ENTITIES_ERROR = "No entities found via 'Aggregation path to related entities'. " +
"Verify the configured relation type and direction.";
public static final ReadinessStatus READY = new ReadinessStatus(true, null);
public static ReadinessStatus notReady(String errorMsg) {
return new ReadinessStatus(false, errorMsg);
}
public static ReadinessStatus from(List<String> emptyOrMissingArguments) {
if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) {
@ -118,13 +124,12 @@ public interface CalculatedFieldState extends Closeable {
}
boolean propagationCtxIsEmpty = emptyOrMissingArguments.remove(PROPAGATION_CONFIG_ARGUMENT);
if (!propagationCtxIsEmpty) {
return new ReadinessStatus(false, MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments));
return notReady(MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments));
}
if (emptyOrMissingArguments.isEmpty()) {
return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_ERROR);
return notReady(MISSING_PROPAGATION_TARGETS_ERROR);
}
return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR +
String.join(", ", emptyOrMissingArguments));
return notReady(MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments));
}
}

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

@ -43,6 +43,8 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString;
@ -61,6 +63,8 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
@Value("${queue.calculated_fields.poll_interval:25}")
private long pollInterval;
@Value("${queue.calculated_fields.pack_processing_timeout:60000}")
private long packProcessingTimeout;
private TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>> stateProducer;
@ -74,21 +78,39 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta
.topic(partitionService.getTopic(queueKey))
.pollInterval(pollInterval)
.msgPackProcessor((msgs, consumer, consumerKey, config) -> {
CountDownLatch completionLatch = new CountDownLatch(msgs.size());
for (TbProtoQueueMsg<CalculatedFieldStateProto> msg : msgs) {
TbCallback callback = new TbCallback() {
@Override
public void onSuccess() {
int processedMsgCount = counter.incrementAndGet();
if (processedMsgCount % 10000 == 0) {
log.info("Processed {} CF state messages", processedMsgCount);
}
completionLatch.countDown();
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to process CF state message: {}", msg, t);
completionLatch.countDown();
}
};
try {
if (msg.getValue() != null) {
processRestoredState(msg.getValue(), consumerKey.partition());
processRestoredState(msg.getValue(), consumerKey.partition(), callback);
} else {
processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition());
processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition(), callback);
}
} catch (Throwable t) {
log.error("Failed to process state message: {}", msg, t);
callback.onFailure(t);
}
}
int processedMsgCount = counter.incrementAndGet();
if (processedMsgCount % 10000 == 0) {
log.info("Processed {} calculated field state msgs", processedMsgCount);
}
boolean success = completionLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS);
if (!success) {
log.error("Timeout to process CF state messages pack of size {}", msgs.size());
}
})
.consumerCreator((queueConfig, tpi) -> queueFactory.createCalculatedFieldStateConsumer())

15
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java

@ -62,11 +62,22 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS
public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) {
if (stateService.getPartitions().isEmpty()) {
cfRocksDb.forEach((key, value) -> {
CalculatedFieldStateProto stateMsg;
try {
processRestoredState(CalculatedFieldStateProto.parseFrom(value), null);
stateMsg = CalculatedFieldStateProto.parseFrom(value);
} catch (Exception e) {
log.error("[{}] Failed to process restored state", key, e);
log.error("Failed to parse CalculatedFieldStateProto for key {}", key, e);
return;
}
processRestoredState(stateMsg, null, new TbCallback() {
@Override
public void onSuccess() {}
@Override
public void onFailure(Throwable t) {
log.error("Failed to process CF state message: {}", stateMsg, t);
}
});
});
}
super.restore(queueKey, partitions);

32
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -122,20 +122,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs {
}
private void addTsRecord(Long ts, KvEntry value) {
try {
switch (value.getDataType()) {
case LONG -> value.getLongValue().ifPresent(aLong -> tsRecords.put(ts, aLong.doubleValue()));
case DOUBLE -> value.getDoubleValue().ifPresent(aDouble -> tsRecords.put(ts, aDouble));
case BOOLEAN -> value.getBooleanValue().ifPresent(aBoolean -> tsRecords.put(ts, aBoolean ? 1.0 : 0.0));
case STRING -> value.getStrValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
case JSON -> value.getJsonValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString)));
}
} catch (Exception e) {
tsRecords.put(ts, Double.NaN);
log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue());
} finally {
cleanupExpiredRecords();
Double recordValue = getValueForTsRecord(value);
if (recordValue != null) {
tsRecords.put(ts, recordValue);
}
cleanupExpiredRecords();
}
private void addTsRecord(Long ts, double value) {
@ -150,4 +141,19 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs {
tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow);
}
public static Double getValueForTsRecord(KvEntry value) {
try {
return switch (value.getDataType()) {
case LONG -> value.getLongValue().map(Long::doubleValue).orElse(null);
case DOUBLE -> value.getDoubleValue().orElse(null);
case BOOLEAN -> value.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElse(null);
case STRING -> value.getStrValue().map(Double::parseDouble).orElse(null);
case JSON -> value.getJsonValue().map(Double::parseDouble).orElse(null);
};
} catch (Exception e) {
log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue());
return Double.NaN;
}
}
}

40
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java

@ -41,6 +41,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported;
import java.util.ArrayList;
import java.util.HashMap;
@ -52,16 +53,17 @@ import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx.DISABLED_INTERVAL_VALUE;
import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState.ReadinessStatus.MISSING_AGGREGATION_ENTITIES_ERROR;
@Slf4j
@Getter
public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState {
public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported {
@Setter
@Getter
private long lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS;
@Setter
@Getter
private long lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS;
@Setter
private long lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS;
private long deduplicationIntervalMs = DISABLED_INTERVAL_VALUE;
private Map<String, AggMetric> metrics;
@ -103,13 +105,24 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
@Override
public void reset() { // must reset everything dependent on arguments
super.reset();
resetScheduledRefreshTs();
lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS;
lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS;
lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS;
metrics = null;
}
public void updateLastRelatedEntitiesRefreshTs() {
@Override
public void resetScheduledRefreshTs() {
lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS;
}
@Override
public long getLastScheduledRefreshTs() {
return lastRelatedEntitiesRefreshTs;
}
@Override
public void updateScheduledRefreshTs() {
lastRelatedEntitiesRefreshTs = System.currentTimeMillis();
}
@ -127,7 +140,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
public List<EntityId> checkRelatedEntities(List<EntityId> relatedEntities) {
Map<EntityId, Map<String, ArgumentEntry>> entityInputs = prepareInputs();
findOutdatedEntities(entityInputs, relatedEntities).forEach(this::cleanupEntityData);
updateLastRelatedEntitiesRefreshTs();
updateScheduledRefreshTs();
return findMissingEntities(entityInputs, relatedEntities);
}
@ -165,6 +178,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
});
lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS;
lastArgsRefreshTs = System.currentTimeMillis();
readinessStatus = checkReadiness();
}
public void scheduleReevaluation() {
@ -276,4 +290,18 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat
record EntityArgument(EntityInfo entity, JsonNode entityArguments) {}
@Override
protected ReadinessStatus checkReadiness() {
if (arguments == null) {
return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR);
}
for (String requiredArgumentKey : requiredArguments) {
ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey);
if (argumentEntry == null || argumentEntry.isEmpty()) {
return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR);
}
}
return ReadinessStatus.READY;
}
}

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java

@ -205,7 +205,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
handleExpiredInterval(intervalEntry, args, results);
expiredIntervals.add(intervalEntry);
} else if (now - startTs >= intervalEntry.getIntervalDuration()) {
handleActiveInterval(ctx.getCfCheckReevaluationInterval(), intervalEntry, args, results);
handleActiveInterval(ctx.getCfCheckReevaluationIntervalMillis(), intervalEntry, args, results);
if (watermarkDuration == 0) {
expiredIntervals.add(intervalEntry);
}
@ -288,7 +288,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt
}
if (!metricsNode.isEmpty()) {
ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", interval.getEndTs() - 1);
resultNode.put("ts", interval.getStartTs());
resultNode.set("values", metricsNode);
result.add(resultNode);

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

@ -94,8 +94,6 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private Alarm currentAlarm;
private boolean initialFetchDone;
// TODO: deprecate device profile node, describe the differences and improvements
public AlarmCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@ -358,7 +356,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
newAlarm.setAcknowledged(false);
newAlarm.setCleared(false);
newAlarm.setSeverity(severity);
long startTs = latestTimestamp;
long startTs = getLatestTimestamp();
long currentTime = System.currentTimeMillis();
if (startTs <= 0L || startTs > currentTime) {
startTs = currentTime;

29
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -21,8 +21,6 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates;
@ -52,11 +50,9 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Ent
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE;
@Getter
@Setter
@Slf4j
@EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported {
private long lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS;
@ -114,10 +110,14 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
return;
}
GeofencingTransitionEvent transitionEvent = eval.transition();
if (transitionEvent == null && !firstEval) {
return;
if (transitionEvent == null) {
if (!firstEval) {
return;
}
transitionEvent = eval.status() == INSIDE ?
GeofencingTransitionEvent.ENTERED :
GeofencingTransitionEvent.LEFT;
}
transitionEvent = transitionEvent == null ? GeofencingTransitionEvent.LEFT : transitionEvent;
EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType());
@ -147,10 +147,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
@Override
public void reset() {
super.reset();
resetScheduledRefreshTs();
}
@Override
public void resetScheduledRefreshTs() {
lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS;
}
public void updateLastDynamicArgumentsRefreshTs() {
@Override
public long getLastScheduledRefreshTs() {
return lastDynamicArgumentsRefreshTs;
}
@Override
public void updateScheduledRefreshTs() {
lastDynamicArgumentsRefreshTs = System.currentTimeMillis();
}

6
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java

@ -89,11 +89,7 @@ public class GeofencingZoneState {
// first evaluation
if (this.lastPresence == null) {
this.lastPresence = status;
GeofencingTransitionEvent transition = null;
if (status == GeofencingPresenceStatus.INSIDE) {
transition = GeofencingTransitionEvent.ENTERED;
}
return new GeofencingEvalResult(transition, status);
return new GeofencingEvalResult(null, status);
}
// State changed
if (this.lastPresence != status) {

26
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java

@ -0,0 +1,26 @@
/**
* 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.cf.ctx.state.geofencing;
public interface ScheduledRefreshSupported {
void resetScheduledRefreshTs();
long getLastScheduledRefreshTs();
void updateScheduledRefreshTs();
}

64
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java

@ -19,22 +19,33 @@ import lombok.Data;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfPropagationArg;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Data
public class PropagationArgumentEntry implements ArgumentEntry {
private List<EntityId> propagationEntityIds;
private Set<EntityId> entityIds;
private transient List<EntityId> added;
private transient EntityId removed;
private boolean forceResetPrevious;
private transient boolean forceResetPrevious;
private transient boolean ignoreRemovedEntities;
public PropagationArgumentEntry(List<EntityId> propagationEntityIds) {
this.propagationEntityIds = new ArrayList<>(propagationEntityIds);
public PropagationArgumentEntry() {
this.entityIds = new HashSet<>();
this.added = null;
this.removed = null;
}
public PropagationArgumentEntry(List<EntityId> entityIds) {
this.entityIds = new HashSet<>(entityIds);
}
@Override
@ -44,30 +55,57 @@ public class PropagationArgumentEntry implements ArgumentEntry {
@Override
public Object getValue() {
return propagationEntityIds;
return entityIds;
}
@Override
public boolean updateEntry(ArgumentEntry entry) {
if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) {
if (!(entry instanceof PropagationArgumentEntry updated)) {
throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType());
}
if (propagationArgumentEntry.isEmpty()) {
propagationEntityIds.clear();
} else {
propagationEntityIds = propagationArgumentEntry.getPropagationEntityIds();
if (updated.getAdded() != null) {
return checkAdded(updated.getAdded());
}
if (updated.getRemoved() != null) {
return entityIds.remove(updated.getRemoved());
}
if (updated.isIgnoreRemovedEntities()) {
Set<EntityId> updatedIds = updated.getEntityIds();
if (updatedIds.isEmpty()) {
entityIds.clear();
return false;
}
entityIds.retainAll(updatedIds);
return checkAdded(updatedIds);
}
if (updated.isEmpty()) {
entityIds.clear();
return true;
}
entityIds = updated.getEntityIds();
return true;
}
private boolean checkAdded(Collection<EntityId> updatedIds) {
for (EntityId id : updatedIds) {
if (entityIds.add(id)) {
if (added == null) {
added = new ArrayList<>();
}
added.add(id);
}
}
return added != null && !added.isEmpty();
}
@Override
public boolean isEmpty() {
return CollectionsUtil.isEmpty(propagationEntityIds);
return entityIds.isEmpty();
}
@Override
public TbelCfArg toTbelCfArg() {
return new TbelCfPropagationArg(propagationEntityIds);
return new TbelCfPropagationArg(entityIds);
}
}

80
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java

@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
@ -34,12 +36,15 @@ import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState {
private CalculatedFieldProcessingService cfProcessingService;
public PropagationCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@ -48,14 +53,30 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState
public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) {
this.ctx = ctx;
this.actorCtx = actorCtx;
this.cfProcessingService = ctx.getCfProcessingService();
this.requiredArguments = new ArrayList<>(ctx.getArgNames());
requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT);
this.readinessStatus = checkReadiness(requiredArguments, arguments);
this.readinessStatus = checkReadiness();
if (ctx.isApplyExpressionForResolvedArguments()) {
this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression());
}
}
@Override
public void init(boolean restored) {
super.init(restored);
if (restored) {
cfProcessingService.fetchPropagationArgumentFromDb(ctx, entityId).ifPresent(fromDb -> {
fromDb.setIgnoreRemovedEntities(true);
var updatedArgs = update(Map.of(PROPAGATION_CONFIG_ARGUMENT, fromDb), ctx);
if (updatedArgs.isEmpty()) {
return;
}
ctx.scheduleReevaluation(0L, actorCtx);
});
}
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.PROPAGATION;
@ -64,24 +85,65 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx) {
ArgumentEntry argumentEntry = arguments.get(PROPAGATION_CONFIG_ARGUMENT);
if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry) || propagationArgumentEntry.isEmpty()) {
if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry)) {
return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build());
}
boolean newEntityAdded = propagationArgumentEntry.getAdded() != null;
List<EntityId> entityIds;
if (newEntityAdded) {
entityIds = propagationArgumentEntry.getAdded();
propagationArgumentEntry.setAdded(null);
} else {
if (propagationArgumentEntry.getEntityIds().isEmpty()) {
return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build());
}
entityIds = List.copyOf(propagationArgumentEntry.getEntityIds());
}
if (ctx.isApplyExpressionForResolvedArguments()) {
return Futures.transform(super.performCalculation(updatedArgs, ctx), telemetryCfResult ->
PropagationCalculatedFieldResult.builder()
.propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds())
.entityIds(entityIds)
.result((TelemetryCalculatedFieldResult) telemetryCfResult)
.build(),
MoreExecutors.directExecutor());
}
if (newEntityAdded || CollectionsUtil.isEmpty(updatedArgs)) {
updatedArgs = arguments;
}
return Futures.immediateFuture(PropagationCalculatedFieldResult.builder()
.propagationEntityIds(propagationArgumentEntry.getPropagationEntityIds())
.result(toTelemetryResult(ctx))
.entityIds(entityIds)
.result(toTelemetryResult(ctx, updatedArgs))
.build());
}
private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) {
@Override
protected ReadinessStatus checkReadiness() {
if (ctx.isApplyExpressionForResolvedArguments() || arguments == null) {
return super.checkReadiness();
}
boolean propagationNotEmpty = false;
boolean hasOtherNonEmpty = false;
List<String> emptyArguments = null;
for (String requiredArgumentKey : requiredArguments) {
ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey);
if (argumentEntry == null || argumentEntry.isEmpty()) {
if (emptyArguments == null) {
emptyArguments = new ArrayList<>();
}
emptyArguments.add(requiredArgumentKey);
} else if (PROPAGATION_CONFIG_ARGUMENT.equals(requiredArgumentKey)) {
propagationNotEmpty = true;
} else {
hasOtherNonEmpty = true;
}
}
if (propagationNotEmpty && hasOtherNonEmpty) {
return ReadinessStatus.READY;
}
return ReadinessStatus.from(emptyArguments);
}
private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> updatedArgs) {
Output output = ctx.getOutput();
TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder =
TelemetryCalculatedFieldResult.builder()
@ -89,12 +151,14 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState
.type(output.getType())
.scope(output.getScope());
ObjectNode valuesNode = JacksonUtil.newObjectNode();
arguments.forEach((outputKey, argumentEntry) -> {
updatedArgs.forEach((outputKey, argumentEntry) -> {
if (argumentEntry instanceof PropagationArgumentEntry) {
return;
}
if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) {
JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey);
if (!singleArgumentEntry.isEmpty()) {
JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey);
}
return;
}
throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " +

3
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -258,8 +258,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration();
transportConfiguration.setBootstrap(Collections.emptyList());
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString()));
transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE));
transportConfiguration.setClientLwM2mSettings(new OtherConfiguration());
DeviceProfileData deviceProfileData = new DeviceProfileData();
DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();

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

@ -206,7 +206,6 @@ public class EdgeContextComponent {
@Autowired
private Optional<EdgeStatsCounterService> statsCounterService;
// processors
@Autowired
private AlarmProcessor alarmProcessor;

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

@ -21,6 +21,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
@ -50,8 +51,11 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.domain.DomainInfo;
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.id.AiModelId;
import org.thingsboard.server.common.data.id.AssetId;
@ -76,6 +80,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -88,6 +93,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
@ -127,11 +133,16 @@ import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
public class EdgeMsgConstructorUtils {
@ -252,12 +263,40 @@ public class EdgeMsgConstructorUtils {
return DeviceCredentialsUpdateMsg.newBuilder().setEntity(JacksonUtil.toString(deviceCredentials)).build();
}
public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile) {
return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(deviceProfile))
public static DeviceProfileUpdateMsg constructDeviceProfileUpdatedMsg(UpdateMsgType msgType, DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
String entity = getEntityAndFixLwm2mBootstrapShortServerId(deviceProfile, edgeVersion);
return DeviceProfileUpdateMsg.newBuilder().setMsgType(msgType).setEntity(entity)
.setIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
.setIdLSB(deviceProfile.getId().getId().getLeastSignificantBits()).build();
}
public static String getEntityAndFixLwm2mBootstrapShortServerId(DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber()) {
return JacksonUtil.toString(deviceProfile);
}
JsonNode jsonNode = JacksonUtil.valueToTree(deviceProfile);
JsonNode profileDataNode = jsonNode.get("profileData");
if (profileDataNode != null && profileDataNode.has("transportConfiguration")) {
JsonNode transportConfigNode = profileDataNode.get("transportConfiguration");
JsonNode bootstrapNode = transportConfigNode.get("bootstrap");
if (bootstrapNode != null && bootstrapNode.isArray()) {
for (JsonNode bootstrapServerNode : bootstrapNode) {
if (bootstrapServerNode.isObject()) {
ObjectNode serverObjectNode = (ObjectNode) bootstrapServerNode;
JsonNode isBootstrapNode = serverObjectNode.get("bootstrapServerIs");
boolean isBootstrapServer = isBootstrapNode != null && isBootstrapNode.asBoolean(false);
JsonNode shortServerIdNode = serverObjectNode.get("shortServerId");
if (isBootstrapServer && (shortServerIdNode == null || shortServerIdNode.isNull())) {
serverObjectNode.put("shortServerId", 0);
}
}
}
}
}
return JacksonUtil.toString(jsonNode);
}
public static DeviceProfileUpdateMsg constructDeviceProfileDeleteMsg(DeviceProfileId deviceProfileId) {
return DeviceProfileUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
@ -670,4 +709,128 @@ public class EdgeMsgConstructorUtils {
.setIdLSB(aiModelId.getId().getLeastSignificantBits()).build();
}
public static List<EdgeEvent> mergeAndFilterDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
try {
edgeEvents = removeDownlinkDuplicates(edgeEvents);
List<AttrUpdateMsg> attrUpdateMsgs = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) {
if (EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) {
attrUpdateMsgs.add(new AttrUpdateMsg(edgeEvent.getEntityId(), edgeEvent.getBody()));
}
}
Map<UUID, Map<String, Long>> latestTsByEntityAndKey = computeLatestTsByEntityAndKey(attrUpdateMsgs);
List<EdgeEvent> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) {
if (!EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) {
result.add(edgeEvent);
continue;
}
Map<String, Long> latestByKey = latestTsByEntityAndKey.get(edgeEvent.getEntityId());
JsonNode filteredBody = filterAttributesBody(edgeEvent.getBody(), latestByKey);
if (filteredBody == null) {
continue;
}
result.add(createFilteredEdgeEvent(edgeEvent, filteredBody));
}
result.sort(Comparator.comparingLong(EdgeEvent::getSeqId));
return result;
} catch (Exception e) {
log.warn("Can't merge downlink duplicates, edgeEvents [{}]", edgeEvents, e);
return edgeEvents;
}
}
private static AttrsTs extractAttributes(JsonNode body) {
if (body == null) {
return new AttrsTs(0L, List.of());
}
String bodyStr = JacksonUtil.toString(body);
var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
long ts = jsonObject.get("ts").getAsLong();
var kv = jsonObject.getAsJsonObject("kv");
List<AttributeKvEntry> attrs = JsonConverter.convertToAttributes(
JsonUtils.getJsonObject(
JsonConverter.convertToAttributesProto(kv).getKvList()
), ts);
return new AttrsTs(ts, attrs);
}
private static JsonNode filterAttributesBody(JsonNode body, Map<String, Long> latestByKey) {
if (body == null || latestByKey == null || latestByKey.isEmpty()) {
return null;
}
String bodyStr = JacksonUtil.toString(body);
JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject();
long ts = jsonObject.get("ts").getAsLong();
JsonObject kv = jsonObject.getAsJsonObject("kv");
for (Iterator<Map.Entry<String, JsonElement>> it = kv.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, JsonElement> e = it.next();
Long latestTs = latestByKey.get(e.getKey());
if (latestTs == null || !latestTs.equals(ts)) {
it.remove();
}
}
if (kv.isEmpty()) {
return null;
}
return JacksonUtil.toJsonNode(jsonObject.toString());
}
private static Map<UUID, Map<String, Long>> computeLatestTsByEntityAndKey(List<AttrUpdateMsg> attrUpdateMsgs) {
Map<UUID, Map<String, Long>> latestTsByEntityAndKey = new HashMap<>();
for (AttrUpdateMsg attrUpdateMsg : attrUpdateMsgs) {
UUID entityId = attrUpdateMsg.entityId();
AttrsTs attrsTs = extractAttributes(attrUpdateMsg.body());
Map<String, Long> map = latestTsByEntityAndKey.computeIfAbsent(entityId, id -> new HashMap<>());
long ts = attrsTs.ts();
for (AttributeKvEntry attr : attrsTs.attrs()) {
map.merge(attr.getKey(), ts, Math::max);
}
}
return latestTsByEntityAndKey;
}
private static EdgeEvent createFilteredEdgeEvent(EdgeEvent edgeEvent, JsonNode filteredBody) {
EdgeEvent filtered = new EdgeEvent();
filtered.setSeqId(edgeEvent.getSeqId());
filtered.setTenantId(edgeEvent.getTenantId());
filtered.setEdgeId(edgeEvent.getEdgeId());
filtered.setAction(edgeEvent.getAction());
filtered.setEntityId(edgeEvent.getEntityId());
filtered.setUid(edgeEvent.getUid());
filtered.setType(edgeEvent.getType());
filtered.setBody(filteredBody);
return filtered;
}
private static List<EdgeEvent> removeDownlinkDuplicates(List<EdgeEvent> edgeEvents) {
Set<EventKey> seen = new HashSet<>();
return edgeEvents.stream()
.filter(e -> seen.add(new EventKey(
e.getTenantId(),
e.getAction(),
e.getEntityId(),
e.getType().name(),
(e.getBody() != null ? e.getBody().toString() : "null"))))
.collect(Collectors.toList());
}
private record EventKey(TenantId tenantId,
EdgeEventActionType action,
UUID entityId,
String type,
String body) {
}
private record AttrsTs(long ts, List<AttributeKvEntry> attrs) {
}
private record AttrUpdateMsg(UUID entityId, JsonNode body) {
}
}

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

@ -117,7 +117,7 @@ public abstract class EdgeGrpcSession implements Closeable {
private static final int MAX_DOWNLINK_ATTEMPTS = 3;
private static final String RATE_LIMIT_REACHED = "Rate limit reached";
protected static final ConcurrentLinkedQueue<EdgeEvent> highPriorityQueue = new ConcurrentLinkedQueue<>();
protected final ConcurrentLinkedQueue<EdgeEvent> highPriorityQueue = new ConcurrentLinkedQueue<>();
protected UUID sessionId;
private BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
@ -306,7 +306,8 @@ public abstract class EdgeGrpcSession implements Closeable {
if (isConnected() && !pageData.getData().isEmpty()) {
if (fetcher instanceof GeneralEdgeEventFetcher) {
long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize));
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize));
}
log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
@ -504,7 +505,8 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size()));
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size()));
stopCurrentSendDownlinkMsgsTask(false);
}
} else {
@ -544,7 +546,8 @@ public abstract class EdgeGrpcSession implements Closeable {
try {
if (msg.getSuccess()) {
sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId());
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1));
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1));
log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg);
} else {
log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg());
@ -651,7 +654,8 @@ public abstract class EdgeGrpcSession implements Closeable {
protected List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
List<DownlinkMsg> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) {
List<EdgeEvent> filtered = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(edgeEvents);
for (EdgeEvent edgeEvent : filtered) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent);
DownlinkMsg downlinkMsg = null;
try {
@ -813,7 +817,8 @@ public abstract class EdgeGrpcSession implements Closeable {
}
}
highPriorityQueue.add(edgeEvent);
ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1));
ctx.getStatsCounterService().ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1));
}
protected ListenableFuture<List<Void>> processUplinkMsg(UplinkMsg uplinkMsg) {

13
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AiModelEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher;
@ -64,6 +65,12 @@ public class EdgeSyncCursor {
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));
fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));
fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new AiModelEdgeEventFetcher(ctx.getAiModelService()));
}
Customer publicCustomer = ctx.getCustomerService().findPublicCustomer(edge.getTenantId());
if (publicCustomer != null) {
@ -84,14 +91,10 @@ public class EdgeSyncCursor {
fetchers.add(new NotificationTemplateEdgeEventFetcher(ctx.getNotificationTemplateService()));
fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService()));
fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService()));
fetchers.add(new SystemWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));
fetchers.add(new TenantWidgetTypesEdgeEventFetcher(ctx.getWidgetTypeService()));
fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService()));
fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService()));
// sync device profiles twice to update software and hardware fields
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService()));
fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService()));
}
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java

@ -52,7 +52,8 @@ public class KafkaEdgeEventService extends BaseEdgeEventService {
TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId());
ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build();
producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null);
statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1));
statsCounterService.ifPresent(statsCounterService ->
statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1));
return Futures.immediateFuture(null);
}

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -82,16 +82,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
edgeEvents.add(edgeEvent);
}
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents);
boolean isInterrupted = true;
try {
boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get();
isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get();
if (isInterrupted) {
log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId());
} else {
consumer.commit();
}
} catch (Exception e) {
log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e);
}
if (!isInterrupted) {
consumer.commit();
}
}
@Override

48
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AiModelEdgeEventFetcher.java

@ -0,0 +1,48 @@
/**
* 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.fetch;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.ai.AiModelService;
@AllArgsConstructor
@Slf4j
public class AiModelEdgeEventFetcher extends BasePageableEdgeEventFetcher<AiModel> {
private final AiModelService aiModelService;
@Override
PageData<AiModel> fetchEntities(TenantId tenantId, Edge edge, PageLink pageLink) {
return aiModelService.findAiModelsByTenantId(tenantId, pageLink);
}
@Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AiModel aiModel) {
return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.AI_MODEL,
EdgeEventActionType.ADDED, aiModel.getId(), null);
}
}

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

@ -20,7 +20,6 @@ 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;
@ -30,8 +29,7 @@ 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.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;
@ -53,15 +51,17 @@ public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiMode
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (aiModelUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
return switch (aiModelUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> {
processAiModel(tenantId, aiModelId, aiModelUpdateMsg, edge);
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
}
yield Futures.immediateFuture(null);
}
case ENTITY_DELETED_RPC_MESSAGE -> {
deleteAiModel(tenantId, edge, aiModelId);
yield Futures.immediateFuture(null);
}
default -> handleUnsupportedMsgType(aiModelUpdateMsg.getMsgType());
};
} catch (DataValidationException e) {
return Futures.immediateFailedFuture(e);
} finally {
@ -95,36 +95,21 @@ public class AiModelEdgeProcessor extends BaseAiModelProcessor implements AiMode
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 created = resultPair.getFirst();
if (created) {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
aiModel.ifPresent(model -> pushEntityEventToRuleEngine(tenantId, edge, model, TbMsgType.ENTITY_CREATED));
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
Boolean aiModelNameUpdated = resultPair.getSecond();
if (aiModelNameUpdated) {
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);
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.AI_MODEL;
}
}

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

@ -22,8 +22,10 @@ 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.edge.Edge;
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.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@ -78,4 +80,12 @@ public abstract class BaseAiModelProcessor extends BaseEdgeProcessor {
return Pair.of(isCreated, isNameUpdated);
}
protected void deleteAiModel(TenantId tenantId, Edge edge, AiModelId aiModelId) {
Optional<AiModel> aiModel = edgeCtx.getAiModelService().findAiModelById(tenantId, aiModelId);
if (aiModel.isPresent()) {
edgeCtx.getAiModelService().deleteByTenantIdAndId(tenantId, aiModelId);
pushEntityEventToRuleEngine(tenantId, edge, aiModel.get(), TbMsgType.ENTITY_DELETED);
}
}
}

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

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
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.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
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.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;

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

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory;
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.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;

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

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -44,7 +44,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
@ -237,7 +237,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));
builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion));
}
return builder.build();
}

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId;
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.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@ -102,7 +102,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple
DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId);
if (deviceProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile);
DeviceProfileUpdateMsg deviceProfileUpdateMsg = EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile, edgeVersion);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceProfileUpdateMsg(deviceProfileUpdateMsg)
@ -141,5 +141,4 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple
public EdgeEventType getEdgeEventType() {
return EdgeEventType.DEVICE_PROFILE;
}
}

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

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.EntityViewId;
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.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg;

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

@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;

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

@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;

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

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;

13
application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java

@ -54,7 +54,7 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_P
import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED;
@TbCoreComponent
@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false)
@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true")
@RequiredArgsConstructor
@Service
@Slf4j
@ -70,7 +70,6 @@ public class EdgeStatsService {
@Value("${edges.stats.report-interval-millis:600000}")
private long reportIntervalMillis;
@Scheduled(
fixedDelayString = "${edges.stats.report-interval-millis:600000}",
initialDelayString = "${edges.stats.report-interval-millis:600000}"
@ -80,13 +79,13 @@ public class EdgeStatsService {
long now = System.currentTimeMillis();
long ts = now - (now % reportIntervalMillis);
Map<EdgeId, MsgCounters> countersByEdge = statsCounterService.getCounterByEdge();
Map<EdgeId, Long> lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap();
Map<EdgeId, MsgCounters> countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge());
Map<EdgeId, MsgCounters> countersByEdgeSnapshot = new HashMap<>(statsCounterService.getMsgCountersByEdge());
boolean isKafkaStats = kafkaAdmin.isPresent();
Map<EdgeId, Long> lagByEdgeId = isKafkaStats ? getLagByEdgeId(countersByEdgeSnapshot) : Collections.emptyMap();
countersByEdgeSnapshot.forEach((edgeId, counters) -> {
TenantId tenantId = counters.getTenantId();
if (kafkaAdmin.isPresent()) {
if (isKafkaStats) {
counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L));
}
List<TsKvEntry> statsEntries = List.of(
@ -102,7 +101,7 @@ public class EdgeStatsService {
});
}
private Map<EdgeId, Long> getEdgeLagByEdgeId(Map<EdgeId, MsgCounters> countersByEdge) {
private Map<EdgeId, Long> getLagByEdgeId(Map<EdgeId, MsgCounters> countersByEdge) {
Map<EdgeId, String> edgeToTopicMap = countersByEdge.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,

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

@ -103,7 +103,7 @@ import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.mobile.MobileAppDao;
import org.thingsboard.server.dao.notification.NotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationTargetService;

5
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.resource.ImageService;
import org.thingsboard.server.dao.resource.ResourceService;
@ -406,7 +406,8 @@ public class InstallScripts {
Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR);
loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE, null);
loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION);
loadSystemResources(resourcesDir.resolve("js_extensions"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION);
loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.MODULE);
loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD, null);
}

13
application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java

@ -22,12 +22,13 @@ import org.springframework.stereotype.Service;
@Service
@Profile("install")
@Slf4j
public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
implements EntityDatabaseSchemaService {
public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements EntityDatabaseSchemaService {
public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql";
public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql";
public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql";
public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql";
public static final String SCHEMA_VIEWS_SQL = "schema-views.sql";
public static final String SCHEMA_FUNCTIONS_SQL = "schema-functions.sql";
public SqlEntityDatabaseSchemaService() {
super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL);
@ -49,8 +50,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer
@Override
public void createOrUpdateViewsAndFunctions() throws Exception {
log.info("Installing SQL DataBase schema views and functions: " + SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
executeQueryFromFile(SCHEMA_VIEWS_AND_FUNCTIONS_SQL);
log.info("Installing SQL DataBase schema views: " + SCHEMA_VIEWS_SQL);
executeQueryFromFile(SCHEMA_VIEWS_SQL);
log.info("Installing SQL DataBase schema functions: " + SCHEMA_FUNCTIONS_SQL);
executeQueryFromFile(SCHEMA_FUNCTIONS_SQL);
}
}

2
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -69,7 +69,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.resource.ResourceService;

2
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java

@ -21,7 +21,7 @@ import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.Arrays;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import java.util.Base64;
import java.util.Optional;

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -29,11 +29,11 @@ import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoF
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserAuthSettingsDao;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import java.util.Comparator;

2
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;

4
application/src/main/java/org/thingsboard/server/service/security/permission/AccessControlService.java

@ -24,6 +24,10 @@ public interface AccessControlService {
void checkPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException;
boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException;
<I extends EntityId, T extends HasTenantId> void checkPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException;
<I extends EntityId, T extends HasTenantId> boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException;
}

14
application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java

@ -54,6 +54,13 @@ public class DefaultAccessControlService implements AccessControlService {
}
}
@Override
@SuppressWarnings("unchecked")
public boolean hasPermission(SecurityUser user, Resource resource, Operation operation) throws ThingsboardException {
var permissionChecker = getPermissionChecker(user.getAuthority(), resource);
return permissionChecker.hasPermission(user, operation);
}
@Override
@SuppressWarnings("unchecked")
public <I extends EntityId, T extends HasTenantId> void checkPermission(SecurityUser user, Resource resource,
@ -64,6 +71,13 @@ public class DefaultAccessControlService implements AccessControlService {
}
}
@Override
@SuppressWarnings("unchecked")
public <I extends EntityId, T extends HasTenantId> boolean hasPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException {
var permissionChecker = getPermissionChecker(user.getAuthority(), resource);
return permissionChecker.hasPermission(user, operation, entityId, entity);
}
private PermissionChecker getPermissionChecker(Authority authority, Resource resource) throws ThingsboardException {
Permissions permissions = authorityPermissions.get(authority);
if (permissions == null) {

5
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -66,6 +66,11 @@ public class TenantAdminPermissions extends AbstractPermissions {
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@Override
public boolean hasPermission(SecurityUser user, Operation operation) {
return true;
}
@Override
public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) {
if (!user.getTenantId().equals(entity.getTenantId())) {

2
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;

2
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface SystemSecurityService {

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService;

18
application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java

@ -16,7 +16,9 @@
package org.thingsboard.server.service.system;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -34,6 +36,7 @@ import org.thingsboard.server.service.install.update.DefaultDataUpdateService;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@ -53,6 +56,8 @@ import java.util.stream.Stream;
@RequiredArgsConstructor
public class SystemPatchApplier {
private static final String SCHEMA_VIEWS_SQL = "sql/schema-views.sql";
private static final long ADVISORY_LOCK_ID = 7536891047216478431L;
private final JdbcTemplate jdbcTemplate;
@ -86,6 +91,9 @@ public class SystemPatchApplier {
}
try {
updateSqlViews();
log.info("Updated sql database views");
int updated = updateWidgetTypes();
log.info("Updated {} widget types", updated);
@ -124,6 +132,16 @@ public class SystemPatchApplier {
&& packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch;
}
private void updateSqlViews() {
try {
URL schemaViewsUrl = Resources.getResource(SCHEMA_VIEWS_SQL);
String sql = Resources.toString(schemaViewsUrl, Charsets.UTF_8);
jdbcTemplate.execute(sql);
} catch (IOException e) {
throw new RuntimeException("Unable to update database views from schema-views.sql", e);
}
}
private int updateWidgetTypes() {
AtomicInteger updated = new AtomicInteger();
Path widgetTypesDir = installScripts.getWidgetTypesDir();

4
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.ByteString;
import org.thingsboard.server.exception.EntitiesLimitExceededException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
@ -76,7 +77,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.exception.EntitiesLimitException;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -398,7 +398,7 @@ public class DefaultTransportApiService implements TransportApiService {
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e);
throw new RuntimeException(e);
} catch (EntitiesLimitException e) {
} catch (EntitiesLimitExceededException e) {
log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage());
return TransportApiResponseMsg.newBuilder()
.setGetOrCreateDeviceResponseMsg(

37
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java

@ -33,6 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProt
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.EntityIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto;
import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto;
import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto;
@ -57,9 +58,11 @@ import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
@ -67,6 +70,8 @@ import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT;
public class CalculatedFieldUtils {
public static CalculatedFieldIdProto toProto(CalculatedFieldId cfId) {
@ -105,10 +110,15 @@ public class CalculatedFieldUtils {
case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry));
case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry));
case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry));
case PROPAGATION -> builder.addAllPropagationEntityIds(toPropagationEntityIdsProto((PropagationArgumentEntry) argEntry));
case RELATED_ENTITIES -> {
RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry;
relatedEntitiesArgumentEntry.getEntityInputs()
.forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry)));
Map<EntityId, ArgumentEntry> entityInputs = relatedEntitiesArgumentEntry.getEntityInputs();
if (entityInputs.isEmpty()) {
builder.addSingleValueArguments(SingleValueArgumentProto.newBuilder().setArgName(argName).build());
} else {
entityInputs.forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry)));
}
}
case ENTITY_AGGREGATION -> {
EntityAggregationArgumentEntry entityAggregationArgumentEntry = (EntityAggregationArgumentEntry) argEntry;
@ -133,6 +143,10 @@ public class CalculatedFieldUtils {
return builder.build();
}
private static List<EntityIdProto> toPropagationEntityIdsProto(PropagationArgumentEntry argEntry) {
return argEntry.getEntityIds().stream().map(ProtoUtils::toProto).collect(Collectors.toList());
}
private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) {
return AlarmRuleStateProto.newBuilder()
.setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse(""))
@ -231,7 +245,10 @@ public class CalculatedFieldUtils {
Map<String, Map<EntityId, ArgumentEntry>> arguments = new HashMap<>();
proto.getSingleValueArgumentsList().forEach(argProto -> {
SingleValueArgumentEntry entry = fromSingleValueArgumentProto(argProto);
arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()).put(entry.getEntityId(), entry);
Map<EntityId, ArgumentEntry> entityInputs = arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>());
if (entry.getEntityId() != null) {
entityInputs.put(entry.getEntityId(), entry);
}
});
arguments.forEach((argName, entityInputs) -> {
relatedEntitiesAggState.getArguments().put(argName, new RelatedEntitiesArgumentEntry(entityInputs, false));
@ -261,13 +278,13 @@ public class CalculatedFieldUtils {
state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto)));
switch (type) {
case SCRIPT -> {
proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
}
case GEOFENCING -> {
proto.getGeofencingArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto)));
case SCRIPT -> proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
case GEOFENCING -> proto.getGeofencingArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto)));
case PROPAGATION -> {
List<EntityId> propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList();
state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds));
}
case ALARM -> {
AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state;

2
application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java

@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mInstance;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve;
import org.thingsboard.server.common.data.util.TbDDFFileParser;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;

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

@ -516,14 +516,14 @@ actors:
response_timeout_ms: "${ACTORS_RPC_RESPONSE_TIMEOUT_MS:30000}"
# Close transport session if RPC delivery timed out. If enabled, RPC will be reverted to the queued state.
# Note:
# - For MQTT transport:
# - QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
# - QoS level 1: This feature applies, as an acknowledgment is expected.
# - QoS level 2: Unsupported.
# - For CoAP transport:
# - Confirmable requests: This feature applies, as delivery confirmation is expected.
# - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
# - For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.
# <ul><li> For MQTT transport:
# <ul><li> QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.</li>
# <li> QoS level 1: This feature applies, as an acknowledgment is expected.</li>
# <li> QoS level 2: Unsupported.</li></ul></li>
# <li> For CoAP transport:
# <ul><li> Confirmable requests: This feature applies, as delivery confirmation is expected.</li>
# <li> Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.</li></ul></li>
# <li> For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.</li></ul>
close_session_on_rpc_delivery_timeout: "${ACTORS_RPC_CLOSE_SESSION_ON_RPC_DELIVERY_TIMEOUT:false}"
statistics:
# Enable/disable actor statistics
@ -733,13 +733,13 @@ redis:
# if set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
sentinel:
# name of the master node
# Name of the master node
master: "${REDIS_MASTER:}"
# comma-separated list of "host:port" pairs of sentinels
# Comma-separated list of "host:port" pairs of sentinels
sentinels: "${REDIS_SENTINELS:}"
# password to authenticate with sentinel
# Password to authenticate with sentinel
password: "${REDIS_SENTINEL_PASSWORD:}"
# if set false will be used pool config build from values of the pool config section
# If set false will be used pool config build from values of the pool config section
useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}"
# db index
db: "${REDIS_DB:0}"
@ -1039,10 +1039,10 @@ transport:
activity:
# This property specifies the strategy for reporting activity events within each reporting period.
# The accepted values are 'FIRST', 'LAST', 'FIRST_AND_LAST' and 'ALL'.
# - 'FIRST': Only the first activity event in each reporting period is reported.
# - 'LAST': Only the last activity event in the reporting period is reported.
# - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
# - 'ALL': All activity events in the reporting period are reported.
# <ul><li> 'FIRST': Only the first activity event in each reporting period is reported.</li>
# <li> 'LAST': Only the last activity event in the reporting period is reported.</li>
# <li> 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.</li>
# <li> 'ALL': All activity events in the reporting period are reported.</li></ul>
reporting_strategy: "${TB_TRANSPORT_ACTIVITY_REPORTING_STRATEGY:LAST}"
json:
# Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON
@ -1160,15 +1160,15 @@ transport:
dtls:
# RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000
retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}"
# CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2
# Default: off
# LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2
# Default: off. <br>
# Control usage of DTLS connection ID length (CID).
# - 'off' to deactivate it.
# - 'on' to activate Connection ID support (same as CID 0 or more 0).
# - A positive value defines generated CID size in bytes.
# - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
# - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used
# <ul> <li> 'off' to deactivate it. </li>
# <li> 'on' to activate Connection ID support (same as CID 0 or more 0). </li>
# <li> A positive value defines generated CID size in bytes.</li>
# <li> A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). </li>
# <li> A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used </li>
# <li> A value that are > 4: MultiNodeConnectionIdGenerator is used </li> </ul>
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}"
server:
# LwM2M Server ID
@ -1351,14 +1351,14 @@ coap:
# CoAP DTLS bind port
bind_port: "${COAP_DTLS_BIND_PORT:5684}"
# CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2
# Default: off
# Default: off. <br>
# Control usage of DTLS connection ID length (CID).
# - 'off' to deactivate it.
# - 'on' to activate Connection ID support (same as CID 0 or more 0).
# - A positive value defines generated CID size in bytes.
# - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
# - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
# - A value that are > 4: MultiNodeConnectionIdGenerator is used
# <ul> <li> 'off' to deactivate it. </li>
# <li> 'on' to activate Connection ID support (same as CID 0 or more 0). </li>
# <li> A positive value defines generated CID size in bytes.</li>
# <li> A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).</li>
# <li> A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used</li>
# <li> A value that are > 4: MultiNodeConnectionIdGenerator is used</li> </ul>
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}"
# Specify the MTU (Maximum Transmission Unit).
# Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU.
@ -1375,13 +1375,13 @@ coap:
# In order to negotiate smaller maximum fragment lengths,
# clients MAY include an extension of type "max_fragment_length" in the (extended) client hello.
# The "extension_data" field of this extension SHALL contain:
# enum {
# <pre> enum {
# 2^9(1) == 512,
# 2^10(2) == 1024,
# 2^11(3) == 2048,
# 2^12(4) == 4096,
# (255)
# } MaxFragmentLength;
# } MaxFragmentLength; </pre>
# TLS already requires clients and servers to support fragmentation of handshake messages.
max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}"
# Server DTLS credentials
@ -1745,6 +1745,8 @@ queue:
print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}"
# Time to wait for the stats-loading requests to Kafka to finish
kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}"
# Topics cache TTL in milliseconds. 5 minutes by default
topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}"
partitions:
hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256
transport_api:

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

@ -799,23 +799,22 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
// --- no restrictedZonesEvent as no transition happened yet
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
// --- no transition events as no transitions happened yet
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE")
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
// --- delete attributes reported in previous evaluation
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus", String.class);
doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesStatus,restrictedZonesStatus", String.class);
// --- Update restrictedZone by 'restrictedZone' attribute update
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE,
@ -909,17 +908,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3);
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE")
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE");
});
@ -1014,16 +1012,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
var geofencingConfiguration = (GeofencingCalculatedFieldConfiguration) configuration;
assertThat(geofencingConfiguration.isScheduledUpdateEnabled()).isTrue();
// --- Assert initial evaluation (ENTERED) ---
// --- Assert initial evaluation ---
await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
assertThat(attrs).isNotNull().isNotEmpty().hasSize(1);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
.containsEntry("allowedZonesStatus", "INSIDE");
assertThat(m).containsEntry("allowedZonesStatus", "INSIDE");
});
// --- Move device OUTSIDE Zone A (expect LEFT) ---
@ -1057,11 +1054,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF);
// --- Same coordinates as before, but now we expect ENTERED since a new zone is registered ---
// --- Same coordinates as before, but now we expect INSIDE group status since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk());
// --- Assert dynamic refresh picks up new relation and flips event back to ENTERED on the next telemetry update ---
// --- Assert dynamic refresh picks up a new relation and flips status back to INSIDE on the next telemetry update ---
await().alias("dynamic refresh rebinds allowedZones")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS)
@ -1069,7 +1066,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED")
assertThat(m).containsEntry("allowedZonesEvent", "LEFT") // attribute from previous eval with outdated ts.
.containsEntry("allowedZonesStatus", "INSIDE");
});
}
@ -1089,7 +1086,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Telemetry on device
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",
JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk());
JacksonUtil.toJsonNode("{\"temperature\":12.5, \"humidity\":85}")).andExpect(status().isOk());
// --- Build CF: PROPAGATION with expression ---
CalculatedField cf = new CalculatedField();
@ -1102,11 +1099,14 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setApplyExpressionToResolvedArguments(true);
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("t", arg));
Argument arg1 = new Argument();
arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setExpression("{\"testResult\": t * 2}");
Argument arg2 = new Argument();
arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("t", arg1, "h", arg2));
cfg.setExpression("return { testResult: (t + h) / 2};");
AttributesOutput output = new AttributesOutput();
output.setScope(AttributeScope.SERVER_SCOPE);
@ -1125,8 +1125,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult");
assertThat(attrs1).isNotNull();
assertThat(attrs2).isNotNull();
assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0);
assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0);
assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(48.75);
assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(48.75);
});
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
@ -1148,7 +1148,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult");
assertThat(attrs1).isNullOrEmpty();
assertThat(attrs2).isNotNull();
assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50);
assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(55);
});
}
@ -1167,7 +1167,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Telemetry on device
long ts = System.currentTimeMillis() - 300000L;
postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts));
postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5, \"humidity\":85}}", ts));
// --- Build CF: PROPAGATION without expression ---
CalculatedField cf = new CalculatedField();
@ -1180,9 +1180,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE));
cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("temperatureComputed", arg));
Argument arg1 = new Argument();
arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
Argument arg2 = new Argument();
arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null));
cfg.setArguments(Map.of("temperatureComputed", arg1, "humidityComputed", arg2));
TimeSeriesOutput output = new TimeSeriesOutput();
output.setStrategy(new TimeSeriesImmediateOutputStrategy(0, true, true, true, true));
@ -1197,14 +1200,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed");
ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed");
ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed");
ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed");
assertThat(telemetry1).isNotNull();
assertThat(telemetry2).isNotNull();
assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5);
assertThat(telemetry1.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry1.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5);
assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
});
String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s",
@ -1212,10 +1219,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE
);
doDelete(deleteUrl).andExpect(status().isOk());
doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk());
doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed,humidityComputed&deleteAllDataForKeys=true").andExpect(status().isOk());
// Update telemetry on device
long newTs = System.currentTimeMillis() - 300000L;
// Update telemetry on the device
long newTs = ts + 300000L;
postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs));
// --- Assert propagated calculation (arguments-only mode after update) ---
@ -1223,14 +1230,37 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed");
ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed");
ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed");
ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed");
assertThat(telemetry1).isNotNull();
assertThat(telemetry2).isNotNull();
assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance);
assertThat(telemetry1.get("humidityComputed").get(0).get("value")).isEqualTo(NullNode.instance);
assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25);
// TS for humidity is not updated -> expected
assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts));
assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
});
Asset asset3 = createAsset("Propagated Asset 3", null);
EntityRelation rel3 = new EntityRelation(asset3.getId(), device.getId(), EntityRelation.CONTAINS_TYPE);
doPost("/api/relation", rel3).andExpect(status().isOk());
// --- Assert propagated calculation (arguments-only mode after update) ---
await().alias("propagation args-only to new entity after relation creation")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed,humidityComputed");
assertThat(telemetry).isNotNull();
assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25);
assertThat(telemetry.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs));
assertThat(telemetry.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85);
});
}
@Test
@ -1282,6 +1312,83 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long now = System.currentTimeMillis();
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3))));
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1))));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
calculatedField.setType(CalculatedFieldType.SCRIPT);
calculatedField.setName("Script CF");
calculatedField.setDebugSettings(DebugSettings.all());
ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration();
ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
Argument argumentA = new Argument();
argumentA.setRefEntityKey(refEntityKeyA);
Argument argumentB = new Argument();
ReferencedEntityKey refEntityKeyB = new ReferencedEntityKey("b", ArgumentType.TS_ROLLING, null);
argumentB.setTimeWindow(TimeUnit.MINUTES.toMillis(10));
argumentB.setLimit(1000);
argumentB.setRefEntityKey(refEntityKeyB);
config.setArguments(Map.of("a", argumentA, "b", argumentB));
config.setExpression("""
return {
"latestA": a,
"avgB": b.avg
};
""");
config.setOutput(new TimeSeriesOutput());
calculatedField.setConfiguration(config);
doPost("/api/calculatedField", calculatedField, CalculatedField.class);
await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB");
assertThat(result).isNotNull();
assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("5");
assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("""
[{
"ts": %s,
"values": {
"a": 6,
"b": 100
}
}, {
"ts": %s,
"values": {
"a": 7,
"b": 200
}
}, {
"ts": %s,
"values": {
"a": 8,
"b": 300
}
}]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5))));
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB");
assertThat(result).isNotNull();
assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("7");
assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("126.0");
});
}
@Test
public void testSimpleCalculatedFieldWhenSkipRuleEngineOutputProcessing() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");

5
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -1209,7 +1209,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() <
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() <
System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
@ -1441,7 +1441,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
EntityType entityType,
List<UUID> entities,
List<String> names) throws Exception {
return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" +
return doGetTypedWithPageLink("/api/calculatedFields?" +
(type != null ? "types=" + type + "&" : "") +
(entityType != null ? "entityType=" + entityType + "&" : "") +
(entities != null ? "entities=" + String.join(",",
entities.stream().map(UUID::toString).toList()) + "&" : "") +

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

@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;

38
application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java

@ -42,13 +42,17 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
@ -195,6 +199,38 @@ public class AssetProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName());
}
@Test
public void testFindAssetProfileInfoByIds() throws Exception {
List<AssetProfileInfo> assetProfiles = new ArrayList<>();
for (int i = 0; i < 20; i++) {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i);
assetProfile.setTenantId(savedTenant.getId());
assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfileInfo.class));
}
List<AssetProfileInfo> expected = assetProfiles.subList(5, 15);
String idsParam = expected.stream()
.map(ap -> ap.getId().getId().toString())
.collect(Collectors.joining(","));
AssetProfileInfo[] foundAssetProfileInfos = doGet("/api/assetProfileInfos?assetProfileIds=" + idsParam, AssetProfileInfo[].class);
Assert.assertNotNull(foundAssetProfileInfos);
Assert.assertEquals(expected.size(), foundAssetProfileInfos.length);
Map<UUID, AssetProfileInfo> infoById = Arrays.stream(foundAssetProfileInfos)
.collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
for (AssetProfileInfo assetProfileInfo : expected) {
UUID id = assetProfileInfo.getId().getId();
AssetProfileInfo info = infoById.get(id);
Assert.assertNotNull("AssetProfileInfo not found for id " + id, info);
Assert.assertEquals(assetProfileInfo.getId(), info.getId());
Assert.assertEquals(assetProfileInfo.getName(), info.getName());
}
}
@Test
public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception {
AssetProfile assetProfile = createAssetProfile("Asset profile 1");

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

@ -259,6 +259,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
List<CalculatedFieldInfo> allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
allCalculatedFields = getCalculatedFields(null, null, null, null);
assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField);
List<CalculatedFieldInfo> profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE,
EntityType.DEVICE_PROFILE, null, null);

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

@ -44,12 +44,17 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.customer.CustomerDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -261,6 +266,40 @@ public class CustomerControllerTest extends AbstractControllerTest {
.andExpect(status().isOk());
}
@Test
public void testFindCustomersByIds() throws Exception {
List<Customer> savedCustomers = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Customer customer = new Customer();
customer.setTitle("My customer " + i);
savedCustomers.add(doPost("/api/customer", customer, Customer.class));
}
String idsParam = savedCustomers.stream()
.map(c -> c.getId().getId().toString())
.collect(Collectors.joining(","));
Customer[] foundCustomers = doGet("/api/customers?customerIds=" + idsParam, Customer[].class);
Assert.assertNotNull(foundCustomers);
Assert.assertEquals(savedCustomers.size(), foundCustomers.length);
Map<UUID, Customer> foundById = Arrays.stream(foundCustomers)
.collect(Collectors.toMap(c -> c.getId().getId(), Function.identity()));
for (Customer savedCustomer : savedCustomers) {
UUID id = savedCustomer.getId().getId();
Customer foundCustomer = foundById.get(id);
Assert.assertNotNull("Customer not found for id " + id, foundCustomer);
Assert.assertEquals(savedCustomer, foundCustomer);
}
for (Customer savedCustomer : savedCustomers) {
doDelete("/api/customer/" + savedCustomer.getId().getId().toString())
.andExpect(status().isOk());
}
}
@Test
public void testDeleteCustomer() throws Exception {
Customer customer = new Customer();

41
application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java

@ -51,13 +51,16 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.dashboard.DashboardDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@ -155,6 +158,42 @@ public class DashboardControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedDashboard, foundDashboard);
}
@Test
public void testFindDashboardInfosByIds() throws Exception {
List<Dashboard> dashboards = new ArrayList<>();
for (int i = 0; i < 20; i++) {
Dashboard dashboard = new Dashboard();
dashboard.setTitle("My dashboard " + i);
dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class));
}
List<Dashboard> expected = dashboards.subList(5, 15);
String idsParam = expected.stream()
.map(d -> d.getId().getId().toString())
.collect(Collectors.joining(","));
DashboardInfo[] result = doGet(
"/api/dashboards?dashboardIds=" + idsParam,
DashboardInfo[].class
);
Assert.assertNotNull(result);
Assert.assertEquals(expected.size(), result.length);
Map<UUID, DashboardInfo> infoById = Arrays.stream(result)
.collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
for (Dashboard dashboard : expected) {
UUID id = dashboard.getId().getId();
DashboardInfo info = infoById.get(id);
Assert.assertNotNull("DashboardInfo not found for id " + id, info);
Assert.assertEquals(dashboard.getId(), info.getId());
Assert.assertEquals(dashboard.getTitle(), info.getTitle());
}
}
@Test
public void testDeleteDashboard() throws Exception {
Dashboard dashboard = new Dashboard();

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

@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumn
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;

42
application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java

@ -52,13 +52,17 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@ -214,6 +218,42 @@ public class DeviceProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType());
}
@Test
public void testFindDeviceProfileInfosByIds() throws Exception {
List<DeviceProfile> deviceProfiles = new ArrayList<>();
for (int i = 0; i < 20; i++) {
DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile " + i);
deviceProfiles.add(saveDeviceProfile(deviceProfile));
}
List<DeviceProfile> expected = deviceProfiles.subList(5, 15);
String idsParam = expected.stream()
.map(dp -> dp.getId().getId().toString())
.collect(Collectors.joining(","));
DeviceProfileInfo[] result = doGet(
"/api/deviceProfileInfos?deviceProfileIds=" + idsParam,
DeviceProfileInfo[].class
);
Assert.assertNotNull(result);
Assert.assertEquals(expected.size(), result.length);
Map<UUID, DeviceProfileInfo> infoById = Arrays.stream(result)
.collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
for (DeviceProfile dp : expected) {
UUID id = dp.getId().getId();
DeviceProfileInfo info = infoById.get(id);
Assert.assertNotNull("DeviceProfileInfo not found for id " + id, info);
Assert.assertEquals(dp.getId(), info.getId());
Assert.assertEquals(dp.getName(), info.getName());
Assert.assertEquals(dp.getType(), info.getType());
}
}
@Test
public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null);

2
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.edge.imitator.EdgeImitator;

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

@ -63,16 +63,19 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.entityview.EntityViewDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.concurrent.TimeUnit.HOURS;
@ -148,6 +151,36 @@ public class EntityViewControllerTest extends AbstractControllerTest {
assertEquals(savedView, foundView);
}
@Test
public void testFindEntityViewByIds() throws Exception {
List<EntityView> assetProfiles = new ArrayList<>();
for (int i = 0; i < 20; i++) {
assetProfiles.add(getNewSavedEntityView("Test entity view " + i));
}
List<EntityView> expected = assetProfiles.subList(5, 15);
String idsParam = expected.stream()
.map(ap -> ap.getId().getId().toString())
.collect(Collectors.joining(","));
EntityView[] foundEntityViews = doGet("/api/entityViews?entityViewIds=" + idsParam, EntityView[].class);
Assert.assertNotNull(foundEntityViews);
Assert.assertEquals(expected.size(), foundEntityViews.length);
Map<UUID, EntityView> infoById = Arrays.stream(foundEntityViews)
.collect(Collectors.toMap(info -> info.getId().getId(), Function.identity()));
for (EntityView entityView : expected) {
UUID id = entityView.getId().getId();
EntityView view = infoById.get(id);
Assert.assertNotNull("Entity view not found for id " + id, view);
Assert.assertEquals(entityView.getId(), view.getId());
Assert.assertEquals(entityView.getName(), view.getName());
}
}
@Test
public void testSaveEntityView() throws Exception {
String name = "Test entity view";

2
application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java

@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.ByteBuffer;

45
application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java

@ -47,12 +47,17 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
@ -231,6 +236,44 @@ public class RuleChainControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedRuleChain, foundRuleChain);
}
@Test
public void testFindRuleChainsByIds() throws Exception {
List<RuleChain> ruleChains = new ArrayList<>();
for (int i = 0; i < 20; i++) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName("RuleChain " + i);
ruleChains.add(doPost("/api/ruleChain", ruleChain, RuleChain.class));
}
List<RuleChain> expected = ruleChains.subList(5, 15);
String idsParam = expected.stream()
.map(rc -> rc.getId().getId().toString())
.collect(Collectors.joining(","));
RuleChain[] result = doGet(
"/api/ruleChains?ruleChainIds=" + idsParam,
RuleChain[].class
);
Assert.assertNotNull(result);
Assert.assertEquals(expected.size(), result.length);
Map<UUID, RuleChain> rcById = Arrays.stream(result)
.collect(Collectors.toMap(rc -> rc.getId().getId(), Function.identity()));
for (RuleChain rc : expected) {
UUID id = rc.getId().getId();
RuleChain found = rcById.get(id);
Assert.assertNotNull("RuleChain not found for id " + id, found);
Assert.assertEquals(rc.getId(), found.getId());
Assert.assertEquals(rc.getName(), found.getName());
Assert.assertEquals(rc.getTenantId(), found.getTenantId());
}
}
@Test
public void testDeleteRuleChain() throws Exception {
RuleChain ruleChain = new RuleChain();

2
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;

38
application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java

@ -68,6 +68,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.QueueKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@ -77,6 +78,7 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -187,6 +189,42 @@ public class TenantControllerTest extends AbstractControllerTest {
deleteTenant(savedTenant.getId());
}
@Test
public void testFindTenantsByIds() throws Exception {
loginSysAdmin();
List<Tenant> savedTenants = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Tenant tenant = new Tenant();
tenant.setTitle("My tenant " + i);
savedTenants.add(saveTenant(tenant));
}
String idsParam = savedTenants.stream()
.map(t -> t.getId().getId().toString())
.collect(Collectors.joining(","));
Tenant[] foundTenants = doGet("/api/tenants?tenantIds=" + idsParam, Tenant[].class);
Assert.assertNotNull(foundTenants);
Assert.assertEquals(savedTenants.size(), foundTenants.length);
Map<UUID, Tenant> foundById = Arrays.stream(foundTenants)
.collect(Collectors.toMap(t -> t.getId().getId(), Function.identity()));
for (Tenant savedTenant : savedTenants) {
UUID id = savedTenant.getId().getId();
Tenant foundTenant = foundById.get(id);
Assert.assertNotNull("Tenant not found for id " + id, foundTenant);
Assert.assertEquals(savedTenant, foundTenant);
}
for (Tenant savedTenant : savedTenants) {
deleteTenant(savedTenant.getId());
}
}
@Test
public void testFindTenantInfoById() throws Exception {
loginSysAdmin();

41
application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java

@ -42,8 +42,12 @@ import org.thingsboard.server.queue.TbQueueCallback;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
@ -120,6 +124,43 @@ public class TenantProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName());
}
@Test
public void testFindTenantProfilesByIds() throws Exception {
loginSysAdmin();
List<TenantProfile> tenantProfiles = new ArrayList<>();
for (int i = 0; i < 10; i++) {
TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile " + i);
tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class));
}
List<TenantProfile> expected = tenantProfiles.subList(2, 7);
String idsParam = expected.stream()
.map(tp -> tp.getId().getId().toString())
.collect(Collectors.joining(","));
TenantProfile[] result = doGet(
"/api/tenantProfiles?ids=" + idsParam,
TenantProfile[].class
);
Assert.assertNotNull(result);
Assert.assertEquals(expected.size(), result.length);
Map<UUID, TenantProfile> tpById = Arrays.stream(result)
.collect(Collectors.toMap(tp -> tp.getId().getId(), Function.identity()));
for (TenantProfile tp : expected) {
UUID id = tp.getId().getId();
TenantProfile found = tpById.get(id);
Assert.assertNotNull("TenantProfile not found for id " + id, found);
Assert.assertEquals(tp.getId(), found.getId());
Assert.assertEquals(tp.getName(), found.getName());
}
}
@Test
public void testFindDefaultTenantProfileInfo() throws Exception {
loginSysAdmin();

37
application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java

@ -50,13 +50,17 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.settings.StarredDashboardInfo;
import org.thingsboard.server.common.data.settings.UserDashboardsInfo;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.user.UserDao;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@ -263,6 +267,37 @@ public class UserControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedUser, foundUser);
}
@Test
public void testFindUsersByIds() throws Exception {
loginTenantAdmin();
List<User> savedUsers = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = createTenantAdminUser();
savedUsers.add(doPost("/api/user", user, User.class));
}
String idsParam = savedUsers.stream()
.map(u -> u.getId().getId().toString())
.collect(Collectors.joining(","));
User[] foundUsers = doGet("/api/users?userIds=" + idsParam, User[].class);
Assert.assertNotNull(foundUsers);
Assert.assertEquals(savedUsers.size(), foundUsers.length);
Map<UUID, User> foundById = Arrays.stream(foundUsers)
.collect(Collectors.toMap(u -> u.getId().getId(), Function.identity()));
for (User savedUser : savedUsers) {
User foundUser = foundById.get(savedUser.getId().getId());
Assert.assertNotNull("User not found for id " + savedUser.getId().getId(), foundUser);
foundUser.setAdditionalInfo(savedUser.getAdditionalInfo());
Assert.assertEquals(savedUser, foundUser);
}
}
@Test
public void testSaveUserWithSameEmail() throws Exception {
loginSysAdmin();

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

Loading…
Cancel
Save