Browse Source

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

pull/14107/head
VIacheslavKlimov 1 year ago
parent
commit
d54938b64d
  1. 4
      application/pom.xml
  2. 15
      application/src/main/data/json/system/widget_types/markdown_html_card.json
  3. 2
      application/src/main/data/upgrade/basic/schema_update.sql
  4. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  5. 4
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
  6. 54
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  7. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  8. 60
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  9. 37
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java
  10. 21
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  11. 1
      application/src/main/java/org/thingsboard/server/controller/ImageController.java
  12. 17
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  13. 40
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  14. 4
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  15. 19
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  16. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  17. 43
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  19. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  20. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java
  21. 82
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  22. 31
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  25. 9
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  26. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  27. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  28. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  29. 5
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  30. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  31. 1
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  32. 5
      application/src/main/resources/thingsboard.yml
  33. 35
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  34. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  35. 12
      application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
  36. 66
      application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java
  37. 61
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  38. 23
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  39. 50
      application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java
  40. 23
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  41. 3
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  42. 2
      application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
  43. 83
      application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java
  44. 125
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  45. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
  46. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  47. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java
  48. 29
      common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java
  49. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  50. 6
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  51. 31
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java
  52. 3
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java
  53. 6
      common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
  54. 8
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java
  55. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
  56. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
  57. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
  58. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
  59. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
  60. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
  61. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
  62. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java
  63. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
  64. 58
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
  66. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java
  67. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java
  68. 48
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java
  69. 30
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java
  70. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java
  71. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java
  72. 2
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java
  73. 71
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java
  74. 86
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java
  75. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java
  76. 8
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
  77. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java
  78. 3
      common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java
  79. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java
  80. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java
  81. 24
      common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java
  82. 30
      common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java
  83. 2
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  84. 2
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java
  85. 126
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java
  86. 216
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java
  87. 28
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java
  88. 4
      common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java
  89. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  90. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java
  91. 15
      common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java
  92. 5
      dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java
  93. 28
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  94. 3
      dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
  95. 5
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  96. 72
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  97. 72
      dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java
  98. 2
      dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java
  99. 2
      dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
  100. 4
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java

4
application/pom.xml

@ -419,6 +419,10 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

15
application/src/main/data/json/system/widget_types/markdown_html_card.json

File diff suppressed because one or more lines are too long

2
application/src/main/data/upgrade/basic/schema_update.sql

@ -27,7 +27,7 @@ SET profile_data = jsonb_set(
CASE CASE
WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF'
THEN NULL THEN NULL
ELSE to_jsonb(3600) ELSE to_jsonb(60)
END, END,
'maxRelationLevelPerCfArgument', 'maxRelationLevelPerCfArgument',
CASE CASE

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

@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeStateService; import org.thingsboard.server.dao.rule.RuleNodeStateService;
@ -512,6 +513,10 @@ public class ActorSystemContext {
@Getter @Getter
private ResourceService resourceService; private ResourceService resourceService;
@Autowired
@Getter
private TbResourceDataCache resourceDataCache;
@Lazy @Lazy
@Autowired(required = false) @Autowired(required = false)
@Getter @Getter

4
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java

@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
case CF_REEVALUATE_MSG: case CF_REEVALUATE_MSG:
processor.process((CalculatedFieldReevaluateMsg) msg); processor.process((CalculatedFieldReevaluateMsg) msg);
break; break;
@ -97,4 +94,5 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
void logProcessingException(Exception e) { void logProcessingException(Exception e) {
log.warn("[{}][{}] Processing failure", tenantId, processor.entityId, e); log.warn("[{}][{}] Processing failure", tenantId, processor.entityId, e);
} }
} }

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

@ -52,6 +52,7 @@ 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.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; 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.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -263,18 +264,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} }
public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException {
log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId());
CalculatedFieldState currentState = states.get(msg.getCfId());
if (currentState == null) {
log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId());
} else {
currentState.setDirty(true);
log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId());
}
msg.getCallback().onSuccess();
}
public void process(CalculatedFieldReevaluateMsg msg) throws CalculatedFieldException { public void process(CalculatedFieldReevaluateMsg msg) throws CalculatedFieldException {
CalculatedFieldId cfId = msg.getCfCtx().getCfId(); CalculatedFieldId cfId = msg.getCfCtx().getCfId();
CalculatedFieldState state = states.get(cfId); CalculatedFieldState state = states.get(cfId);
@ -330,12 +319,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state == null) { if (state == null) {
state = createState(ctx); state = createState(ctx);
justRestored = true; justRestored = true;
} else if (state.isDirty()) { } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) {
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId());
try { try {
Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); Map<String, ArgumentEntry> dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId);
dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); dynamicArgsFromDb.forEach(newArgValues::putIfAbsent);
state.setDirty(false); var geofencingState = (GeofencingCalculatedFieldState) state;
geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis());
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
@ -476,7 +466,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList);
} }
private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geoArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) { private Map<String, ArgumentEntry> mapToArguments(EntityId entityId, Map<ReferencedEntityKey, String> argNames, List<String> geofencingArgNames, AttributeScopeProto scope, List<AttributeValueProto> attrDataList) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (AttributeValueProto item : attrDataList) { for (AttributeValueProto item : attrDataList) {
ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
@ -484,7 +474,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (argName == null) { if (argName == null) {
continue; continue;
} }
if (geoArgNames.contains(argName)) { if (geofencingArgNames.contains(argName)) {
arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); arguments.put(argName, new GeofencingArgumentEntry(entityId, item));
continue; continue;
} }
@ -494,26 +484,36 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List<String> removedAttrKeys) {
return mapToArgumentsWithDefaultValue(ctx.getLinkedAndDynamicArgs(entityId), ctx.getArguments(), scope, removedAttrKeys); var argNames = ctx.getLinkedAndDynamicArgs(entityId);
if (argNames.isEmpty()) {
return Collections.emptyMap();
}
List<String> geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames();
return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys);
} }
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List<String> removedAttrKeys) {
return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys);
} }
private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, AttributeScopeProto scope, List<String> removedAttrKeys) { private Map<String, ArgumentEntry> mapToArgumentsWithDefaultValue(Map<ReferencedEntityKey, String> argNames, Map<String, Argument> configArguments, List<String> geofencingArgNames, AttributeScopeProto scope, List<String> removedAttrKeys) {
Map<String, ArgumentEntry> arguments = new HashMap<>(); Map<String, ArgumentEntry> arguments = new HashMap<>();
for (String removedKey : removedAttrKeys) { for (String removedKey : removedAttrKeys) {
ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name()));
String argName = argNames.get(key); String argName = argNames.get(key);
if (argName != null) { if (argName == null) {
Argument argument = configArguments.get(argName); continue;
String defaultValue = (argument != null) ? argument.getDefaultValue() : null; }
arguments.put(argName, StringUtils.isNotEmpty(defaultValue) if (geofencingArgNames.contains(argName)) {
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) arguments.put(argName, new GeofencingArgumentEntry());
: new SingleValueArgumentEntry()); continue;
} }
Argument argument = configArguments.get(argName);
String defaultValue = (argument != null) ? argument.getDefaultValue() : null;
arguments.put(argName, StringUtils.isNotEmpty(defaultValue)
? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null)
: new SingleValueArgumentEntry());
} }
return arguments; return arguments;
} }

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

@ -82,9 +82,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_LINKED_TELEMETRY_MSG: case CF_LINKED_TELEMETRY_MSG:
processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg);
break; break;
case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
default: default:
return false; return false;
} }

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

@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -63,7 +62,6 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -81,7 +79,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldCtx>> entityIdCalculatedFields = new HashMap<>();
private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>(); private final Map<EntityId, List<CalculatedFieldLink>> entityIdCalculatedFieldLinks = new HashMap<>();
private final Map<EntityId, Set<EntityId>> ownerEntities = new HashMap<>(); private final Map<EntityId, Set<EntityId>> ownerEntities = new HashMap<>();
private final Map<CalculatedFieldId, ScheduledFuture<?>> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>();
private ScheduledFuture<?> cfsReevaluationTask; private ScheduledFuture<?> cfsReevaluationTask;
private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldProcessingService cfExecService;
@ -123,8 +120,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.clear(); calculatedFields.clear();
entityIdCalculatedFields.clear(); entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear(); entityIdCalculatedFieldLinks.clear();
cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true));
cfDynamicArgumentsRefreshTasks.clear();
if (cfsReevaluationTask != null) { if (cfsReevaluationTask != null) {
cfsReevaluationTask.cancel(true); cfsReevaluationTask.cancel(true);
cfsReevaluationTask = null; cfsReevaluationTask = null;
@ -328,7 +323,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf); addLinks(cf);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb)); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb));
} }
} }
@ -358,12 +352,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
calculatedFields.put(newCf.getId(), newCfCtx); calculatedFields.put(newCf.getId(), newCfCtx);
List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); List<CalculatedFieldCtx> oldCfList = entityIdCalculatedFields.get(newCf.getEntityId());
boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx);
if (hasSchedulingConfigChanges) {
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx);
}
List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>(); List<CalculatedFieldCtx> newCfList = new CopyOnWriteArrayList<>();
boolean found = false; boolean found = false;
for (CalculatedFieldCtx oldCtx : oldCfList) { for (CalculatedFieldCtx oldCtx : oldCfList) {
@ -411,19 +399,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx); deleteLinks(cfCtx);
cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb));
} }
private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) {
var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId);
if (existingTask != null) {
existingTask.cancel(false);
String reason = cfDeleted ? "deletion" : "update";
log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason);
}
}
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {
EntityId entityId = msg.getEntityId(); EntityId entityId = msg.getEntityId();
log.debug("Received telemetry msg from entity [{}]", entityId); log.debug("Received telemetry msg from entity [{}]", entityId);
@ -571,43 +549,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result; return result;
} }
private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
CalculatedField cf = cfCtx.getCalculatedField();
if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
return;
}
if (!scheduledCfConfig.isScheduledUpdateEnabled()) {
return;
}
if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) {
log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
return;
}
long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval());
var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
ScheduledFuture<?> scheduledFuture = systemContext
.schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval);
cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture);
log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId());
}
public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) {
log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId());
CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId());
if (cfCtx == null) {
log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId());
cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true);
return;
}
applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb));
}
private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) {
log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback));
}
private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) {
log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(msg); getOrCreateActor(entityId).tell(msg);
@ -694,7 +635,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// We use copy on write lists to safely pass the reference to another actor for the iteration. // We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
} }
private void initCalculatedFieldLink(CalculatedFieldLink link) { private void initCalculatedFieldLink(CalculatedFieldLink link) {

37
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java

@ -1,37 +0,0 @@
/**
* 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.actors.calculatedField;
import lombok.Data;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
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;
@Data
public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final CalculatedFieldId cfId;
private final TbCallback callback;
@Override
public MsgType getMsgType() {
return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG;
}
}

21
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getResourceService(); return mainCtx.getResourceService();
} }
@Override
public TbResourceDataCache getTbResourceDataCache() {
return mainCtx.getResourceDataCache();
}
@Override @Override
public OtaPackageService getOtaPackageService() { public OtaPackageService getOtaPackageService() {
return mainCtx.getOtaPackageService(); return mainCtx.getOtaPackageService();
@ -1054,7 +1062,18 @@ public class DefaultTbContext implements TbContext {
@Override @Override
public void checkTenantEntity(EntityId entityId) throws TbNodeException { public void checkTenantEntity(EntityId entityId) throws TbNodeException {
if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId);
assertSameTenantId(actualTenantId, entityId);
}
@Override
public <E extends HasId<I> & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException {
TenantId actualTenantId = entity.getTenantId();
assertSameTenantId(actualTenantId, entity.getId());
}
private void assertSameTenantId(TenantId tenantId, EntityId entityId) throws TbNodeException {
if (!getTenantId().equals(tenantId)) {
throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true);
} }
} }

1
application/src/main/java/org/thingsboard/server/controller/ImageController.java

@ -300,6 +300,7 @@ public class ImageController extends BaseController {
tbImageService.putETag(cacheKey, descriptor.getEtag()); tbImageService.putETag(cacheKey, descriptor.getEtag());
var result = ResponseEntity.ok() var result = ResponseEntity.ok()
.header("Content-Type", descriptor.getMediaType()) .header("Content-Type", descriptor.getMediaType())
.header("Content-Security-Policy", "default-src 'none'")
.eTag(descriptor.getEtag()); .eTag(descriptor.getEtag());
if (!cacheKey.isPublic()) { if (!cacheKey.isPublic()) {
result result

17
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; 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.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION;
@ -263,6 +266,20 @@ public class TbResourceController extends BaseController {
} }
} }
@ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@GetMapping(value = "/resource", params = {"resourceIds"})
public List<TbResourceInfo> getSystemOrTenantResourcesByIds(
@Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string")))
@RequestParam("resourceIds") Set<UUID> resourceUuids) throws ThingsboardException {
SecurityUser user = getCurrentUser();
List<TbResourceId> resourceIds = new ArrayList<>();
for (UUID resourceId : resourceUuids) {
resourceIds.add(new TbResourceId(resourceId));
}
return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds);
}
@ApiOperation(value = "Get All Resource Infos (getAllResources)", @ApiOperation(value = "Get All Resource Infos (getAllResources)",
notes = "Returns a page of Resource Info objects owned by tenant. " + notes = "Returns a page of Resource Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)

40
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -32,8 +32,10 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.Region;
@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import static java.util.Collections.singletonMap;
@Component @Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@ -62,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
@Override @Override
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder() return OpenAiChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.apiKey(chatModelConfig.providerConfig().apiKey()) .apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId()) .modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature()) .temperature(chatModelConfig.temperature())
@ -134,7 +143,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// set request timeout from model config // set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) { if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
} }
// set updated retry settings // set updated retry settings
@ -262,6 +271,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build(); .build();
} }
@Override
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
var builder = OllamaChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.numCtx(chatModelConfig.contextLength())
.numPredict(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries());
var auth = chatModelConfig.providerConfig().auth();
if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) {
String credentials = basicAuth.username() + ":" + basicAuth.password();
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) {
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token()));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) {
// do nothing
} else {
throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName());
}
return builder.build();
}
private static Duration toDuration(Integer timeoutSeconds) { private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
} }

4
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService
boolean check(long threshold, long warnThreshold, long value); boolean check(long threshold, long warnThreshold, long value);
} }
private void checkStartOfNextCycle() { public void checkStartOfNextCycle() {
updateLock.lock(); updateLock.lock();
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
myUsageStates.values().forEach(state -> { myUsageStates.values().forEach(state -> {
if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) {
state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth());
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs()); log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs());
} }

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

@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
@ -36,7 +36,6 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
@ -164,19 +163,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration();
return switch (refDynamicSourceConfiguration.getType()) { return switch (refDynamicSourceConfiguration.getType()) {
case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId))); case CURRENT_OWNER -> Futures.immediateFuture(List.of(resolveOwnerArgument(tenantId, entityId)));
case RELATION_QUERY -> { case RELATION_PATH_QUERY -> {
var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration;
if (configuration.isSimpleRelation()) { yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)),
yield switch (configuration.getDirection()) {
case FROM ->
Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
case TO ->
Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
};
}
yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor); configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
} }
}; };

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

@ -58,7 +58,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
boolean entryUpdated; boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) { if (existingEntry == null || newEntry.isForceResetPrevious()) {
validateNewEntry(newEntry); validateNewEntry(key, newEntry);
arguments.put(key, newEntry); arguments.put(key, newEntry);
entryUpdated = true; entryUpdated = true;
} else { } else {
@ -103,7 +103,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
} }
} }
protected void validateNewEntry(ArgumentEntry newEntry) {} protected void validateNewEntry(String key, ArgumentEntry newEntry) {}
private void updateLastUpdateTimestamp(ArgumentEntry entry) { private void updateLastUpdateTimestamp(ArgumentEntry entry) {
long newTs = this.latestTimestamp; long newTs = this.latestTimestamp;

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

@ -51,6 +51,7 @@ import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,6 +60,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; import java.util.stream.Stream;
@Data @Data
@ -93,9 +95,12 @@ public class CalculatedFieldCtx {
private long maxStateSize; private long maxStateSize;
private long maxSingleValueArgumentSize; private long maxSingleValueArgumentSize;
private boolean relationQueryDynamicArguments;
private List<String> mainEntityGeofencingArgumentNames; private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityAndCurrentOwnerGeofencingArgumentNames; private List<String> linkedEntityAndCurrentOwnerGeofencingArgumentNames;
private long scheduledUpdateIntervalMillis;
public CalculatedFieldCtx(CalculatedField calculatedField, public CalculatedFieldCtx(CalculatedField calculatedField,
ActorSystemContext systemContext) { ActorSystemContext systemContext) {
this.calculatedField = calculatedField; this.calculatedField = calculatedField;
@ -118,8 +123,8 @@ public class CalculatedFieldCtx {
var refId = entry.getValue().getRefEntityId(); var refId = entry.getValue().getRefEntityId();
var refKey = entry.getValue().getRefEntityKey(); var refKey = entry.getValue().getRefEntityKey();
if (refId == null) { if (refId == null) {
// TODO: no matchers for this type of source exists yet, so no reason to add to dynamicEntityArguments map.
if (entry.getValue().hasRelationQuerySource()) { if (entry.getValue().hasRelationQuerySource()) {
relationQueryDynamicArguments = true;
continue; continue;
} }
if (entry.getValue().hasOwnerSource()) { if (entry.getValue().hasOwnerSource()) {
@ -150,6 +155,9 @@ public class CalculatedFieldCtx {
}); });
} }
} }
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) {
this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L;
}
this.requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); this.requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
this.tbelInvokeService = systemContext.getTbelInvokeService(); this.tbelInvokeService = systemContext.getTbelInvokeService();
this.relationService = systemContext.getRelationService(); this.relationService = systemContext.getRelationService();
@ -470,6 +478,9 @@ public class CalculatedFieldCtx {
if (cfType == CalculatedFieldType.ALARM && !calculatedField.getName().equals(other.getCalculatedField().getName())) { if (cfType == CalculatedFieldType.ALARM && !calculatedField.getName().equals(other.getCalculatedField().getName())) {
return true; return true;
} }
if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) {
return true;
}
return false; return false;
} }
@ -487,19 +498,37 @@ public class CalculatedFieldCtx {
} }
// TODO: implement rules update logic! // TODO: implement rules update logic!
} }
if (hasGeofencingZoneGroupConfigurationChanges(other)) {
hasChanges = true;
}
return hasChanges; return hasChanges;
} }
public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) {
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups());
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
return refreshTriggerChanged || refreshIntervalChanged;
} }
return false; return false;
} }
public boolean hasRelationQueryDynamicArguments() {
return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1;
}
public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) {
if (!hasRelationQueryDynamicArguments()) {
return false;
}
if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) {
return false;
}
if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) {
return true;
}
return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis;
}
public void stop() { public void stop() {
if (tbelExpressions != null) { if (tbelExpressions != null) {
tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy);

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

@ -47,13 +47,6 @@ public interface CalculatedFieldState {
long getLatestTimestamp(); long getLatestTimestamp();
default void setDirty(boolean dirty) {
}
default boolean isDirty() {
return false;
}
void init(CalculatedFieldCtx ctx); void init(CalculatedFieldCtx ctx);
Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> arguments, CalculatedFieldCtx ctx); Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> arguments, CalculatedFieldCtx ctx);

5
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -43,9 +43,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
protected void validateNewEntry(ArgumentEntry newEntry) { protected void validateNewEntry(String key, ArgumentEntry newEntry) {
if (newEntry instanceof TsRollingArgumentEntry) { if (newEntry instanceof TsRollingArgumentEntry) {
throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " +
"Rolling argument entry is not supported for simple calculated fields.");
} }
} }

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java

@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
} }
public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) {
this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry))); this(Map.of(entityId, ProtoUtils.fromProto(entry)));
} }
public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) { public GeofencingArgumentEntry(Map<EntityId, KvEntry> entityIdkvEntryMap) {
@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType());
} }
if (geofencingArgumentEntry.isEmpty()) {
zoneStates.clear();
return true;
}
boolean updated = false; boolean updated = false;
for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) {
if (updateZone(zoneEntry)) { if (updateZone(zoneEntry)) {
@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry {
zoneStates.put(zoneId, newZoneState); zoneStates.put(zoneId, newZoneState);
return true; return true;
} }
if (newZoneState.getPerimeterDefinition() == null) {
zoneStates.remove(zoneId);
return true;
}
return existingZoneState.update(newZoneState); return existingZoneState.update(newZoneState);
} }

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.cf.ctx.state.geofencing; package org.thingsboard.server.service.cf.ctx.state.geofencing;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -26,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.Coordinates;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
@ -41,8 +43,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -58,7 +58,7 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
private boolean dirty = false; private long lastDynamicArgumentsRefreshTs = -1;
public GeofencingCalculatedFieldState(EntityId entityId) { public GeofencingCalculatedFieldState(EntityId entityId) {
super(entityId); super(entityId);
@ -70,52 +70,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
} }
@Override @Override
public Map<String, ArgumentEntry> update(Map<String, ArgumentEntry> argumentValues, CalculatedFieldCtx ctx) { protected void validateNewEntry(String key, ArgumentEntry newEntry) {
Map<String, ArgumentEntry> updatedArguments = null; switch (key) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
for (var entry : argumentValues.entrySet()) { if (!(newEntry instanceof SingleValueArgumentEntry)) {
String key = entry.getKey(); throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
ArgumentEntry newEntry = entry.getValue(); "Only SINGLE_VALUE type is allowed.");
}
checkArgumentSize(key, newEntry, ctx);
ArgumentEntry existingEntry = arguments.get(key);
boolean entryUpdated;
if (existingEntry == null || newEntry.isForceResetPrevious()) {
entryUpdated = switch (key) {
case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> {
if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only SINGLE_VALUE type is allowed.");
}
arguments.put(key, singleValueArgumentEntry);
yield true;
}
default -> {
if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) {
throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
}
arguments.put(key, geofencingArgumentEntry);
yield true;
}
};
} else {
entryUpdated = existingEntry.updateEntry(newEntry);
} }
if (entryUpdated) { default -> {
if (updatedArguments == null) { if (!(newEntry instanceof GeofencingArgumentEntry)) {
updatedArguments = new HashMap<>(argumentValues.size()); throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " +
"Only GEOFENCING type is allowed.");
} }
updatedArguments.put(key, newEntry);
} }
} }
if (updatedArguments == null) {
updatedArguments = Collections.emptyMap();
}
return updatedArguments;
} }
@Override @Override
@ -127,7 +96,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups(); Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg.getZoneGroups();
ObjectNode resultNode = JacksonUtil.newObjectNode(); ObjectNode valuesNode = JacksonUtil.newObjectNode();
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>(); List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
@ -156,13 +125,14 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
relationFutures.add(f); relationFutures.add(f);
} }
}); });
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode); updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode);
}); });
OutputType outputType = ctx.getOutput().getType();
var result = TelemetryCalculatedFieldResult.builder() var result = TelemetryCalculatedFieldResult.builder()
.type(ctx.getOutput().getType()) .type(outputType)
.scope(ctx.getOutput().getScope()) .scope(ctx.getOutput().getScope())
.result(resultNode) .result(toResultNode(outputType, valuesNode))
.build(); .build();
if (relationFutures.isEmpty()) { if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result); return Futures.immediateFuture(result);
@ -173,7 +143,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
@Override @Override
public void reset(CalculatedFieldCtx ctx) { public void reset(CalculatedFieldCtx ctx) {
super.reset(ctx); super.reset(ctx);
dirty = false; lastDynamicArgumentsRefreshTs = -1;
} }
private Map<String, GeofencingArgumentEntry> getGeofencingArguments() { private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
@ -183,7 +153,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
.collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue()));
} }
private void updateResultNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { private void updateValuesNode(String argumentKey, List<GeofencingEvalResult> zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) {
GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults);
final String eventKey = argumentKey + "Event"; final String eventKey = argumentKey + "Event";
final String statusKey = argumentKey + "Status"; final String statusKey = argumentKey + "Status";
@ -197,6 +167,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
} }
} }
private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) {
if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) {
return valuesNode;
}
ObjectNode resultNode = JacksonUtil.newObjectNode();
resultNode.put("ts", latestTimestamp);
resultNode.set("values", valuesNode);
return resultNode;
}
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) { private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
boolean prevInside = zoneResults.stream() boolean prevInside = zoneResults.stream()

31
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -93,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
@TbCoreComponent @TbCoreComponent
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10;
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>(); private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>(); private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>(); private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); private final ConcurrentMap<EdgeId, Boolean> edgeEventsMigrationProcessed = new ConcurrentHashMap<>();
private final List<EdgeGrpcSession> zombieSessions = new ArrayList<>();
@Value("${edges.rpc.port}") @Value("${edges.rpc.port}")
private int rpcPort; private int rpcPort;
@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!"); log.info("Edge RPC service initialized!");
} }
@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) { private void destroySession(EdgeGrpcSession session) {
try (session) { try (session) {
for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { if (!session.destroy()) {
if (session.destroy()) { log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.",
break; session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
} else { zombieSessions.add(session);
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
}
} }
} }
} }
@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() { private void cleanupZombieSessions() {
try { try {
List<EdgeId> toRemove = new ArrayList<>(); List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) { for (EdgeGrpcSession session : sessions.values()) {
@ -655,9 +650,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
} }
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return true;
} else {
log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].",
zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName());
return false;
}
});
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", e); log.warn("Failed to cleanup kafka sessions", e);
} }
} }
} }

2
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder; entitiesSortOrder = sortOrder;
} }
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
} }
@Override @Override

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

@ -38,6 +38,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@ -87,6 +88,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
ActorSystemContext actorContext, ActorSystemContext actorContext,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
@ -94,7 +96,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
JwtSettingsService jwtSettingsService, JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) { CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService); eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory; this.queueFactory = tbQueueFactory;
this.stateService = stateService; this.stateService = stateService;

9
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService {
@Override @Override
public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) {
TenantId tenantId = resource.getTenantId();
TbResourceId resourceId = resource.getId();
if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { if (resource.getResourceType() == ResourceType.LWM2M_MODEL) {
TenantId tenantId = resource.getTenantId();
log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey());
ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdMSB(tenantId.getId().getMostSignificantBits())
@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
} }
broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED);
} }
@Override @Override
@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService {
ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build();
broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback);
} }
broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED);
} }
private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { private <T> void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) {
@ -592,7 +596,8 @@ public class DefaultTbClusterService implements TbClusterService {
EntityType.TENANT_PROFILE, EntityType.TENANT_PROFILE,
EntityType.DEVICE_PROFILE, EntityType.DEVICE_PROFILE,
EntityType.ASSET_PROFILE, EntityType.ASSET_PROFILE,
EntityType.JOB) EntityType.JOB,
EntityType.TB_RESOURCE)
|| (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
) { ) {

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.resource.ImageCacheKey; import org.thingsboard.server.dao.resource.ImageCacheKey;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
NotificationSchedulerService notificationSchedulerService, NotificationSchedulerService notificationSchedulerService,
NotificationRuleProcessor notificationRuleProcessor, NotificationRuleProcessor notificationRuleProcessor,
TbImageService imageService, TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService, RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) { EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService); eventPublisher, jwtSettingsService);
this.stateService = stateService; this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService; this.localSubscriptionService = localSubscriptionService;

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java

@ -87,7 +87,7 @@ public class DefaultTbEdgeConsumerService extends AbstractConsumerService<ToEdge
public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext, public DefaultTbEdgeConsumerService(TbCoreQueueFactory tbCoreQueueFactory, ActorSystemContext actorContext,
StatsFactory statsFactory, EdgeContextComponent edgeCtx) { StatsFactory statsFactory, EdgeContextComponent edgeCtx) {
super(actorContext, null, null, null, null, null, null, null, null); super(actorContext, null, null, null, null, null, null, null, null, null);
this.edgeCtx = edgeCtx; this.edgeCtx = edgeCtx;
this.stats = new EdgeConsumerStats(statsFactory); this.stats = new EdgeConsumerStats(statsFactory);
this.queueFactory = tbCoreQueueFactory; this.queueFactory = tbCoreQueueFactory;

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg; import org.thingsboard.server.gen.transport.TransportProtos.QueueDeleteMsg;
@ -78,13 +79,14 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
QueueService queueService, QueueService queueService,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService, JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache) { CalculatedFieldCache calculatedFieldCache) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx; this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService; this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService; this.queueService = queueService;

5
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -30,12 +30,14 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -72,6 +74,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbTenantProfileCache tenantProfileCache; protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache; protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache; protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache; protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService; protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService; protected final PartitionService partitionService;
@ -218,6 +221,8 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
} else { } else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId()); calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
} }
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));
} }
eventPublisher.publishEvent(componentLifecycleMsg); eventPublisher.publishEvent(componentLifecycleMsg);

4
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.queue.processing;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
@ -43,12 +44,13 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbTenantProfileCache tenantProfileCache, TbTenantProfileCache tenantProfileCache,
TbDeviceProfileCache deviceProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache, CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, PartitionService partitionService,
ApplicationEventPublisher eventPublisher, ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) { JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
} }
@PostConstruct @PostConstruct

1
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -102,7 +102,6 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
if (result.isSuccess()) { if (result.isSuccess()) {
logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString()); logEntityActionService.logEntityAction(tenantId, resourceId, tbResource, actionType, user, resourceId.toString());
} }
return result; return result;
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE), logEntityActionService.logEntityAction(tenantId, emptyId(EntityType.TB_RESOURCE),

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

@ -682,6 +682,9 @@ cache:
maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled
systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled
tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled
tbResourceData:
timeToLiveInMinutes: "${CACHE_SPECS_RESOURCE_DATA_TTL:10080}" # TB resource data cache TTL
maxSize: "${CACHE_SPECS_RESOURCE_DATA_MAX_SIZE:100000}" # 0 means the cache is disabled
# Spring data parameters # Spring data parameters
spring.data.redis.repositories.enabled: false # Disable this because it is not required. spring.data.redis.repositories.enabled: false # Disable this because it is not required.
@ -1545,7 +1548,7 @@ swagger:
version: "${SWAGGER_VERSION:}" version: "${SWAGGER_VERSION:}"
# The group name (definition) on the API doc UI page. # The group name (definition) on the API doc UI page.
group_name: "${SWAGGER_GROUP_NAME:thingsboard}" group_name: "${SWAGGER_GROUP_NAME:thingsboard}"
# Control the initial display state of API operations and tags (none, list or full) # Control the initial display state of API operations and tags (none or list)
doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" doc_expansion: "${SWAGGER_DOC_EXPANSION:list}"
# Queue configuration parameters # Queue configuration parameters

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

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
@ -46,10 +46,12 @@ import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.controller.CalculatedFieldControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -755,19 +757,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
// Zone groups: ATTRIBUTE on specific assets (one zone per group) // Zone groups: ATTRIBUTE on specific assets (one zone per group)
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone")));
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup));
@ -826,10 +822,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class);
assertThat(foundTenantProfile).isNotNull(); assertThat(foundTenantProfile).isNotNull();
assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull();
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10;
foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF);
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class);
assertThat(savedTenantProfile).isNotNull(); assertThat(savedTenantProfile).isNotNull();
assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF);
loginTenantAdmin(); loginTenantAdmin();
// --- Arrange entities --- // --- Arrange entities ---
@ -864,11 +861,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY));
var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup));
@ -879,7 +873,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setOutput(out); cfg.setOutput(out);
// Enable scheduled refresh with a 6-second interval // Enable scheduled refresh with a 6-second interval
cfg.setScheduledUpdateInterval(6); cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF);
cfg.setScheduledUpdateEnabled(true);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);
@ -930,7 +925,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
relAllowedB.setType("AllowedZone"); relAllowedB.setType("AllowedZone");
doPost("/api/relation", relAllowedB).andExpect(status().isOk()); doPost("/api/relation", relAllowedB).andExpect(status().isOk());
awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); 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 ENTERED since a new zone is registered ---
doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope",

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

@ -160,6 +160,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.queue.memory.InMemoryStorage;
import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.CfRocksDb;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
import org.thingsboard.server.service.security.auth.rest.LoginRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest;
@ -1109,14 +1110,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}); });
} }
protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) {
CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId);
Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states"); Map<CalculatedFieldId, CalculatedFieldState> statesMap = (Map<CalculatedFieldId, CalculatedFieldState>) ReflectionTestUtils.getField(processor, "states");
Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId); CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
return stateDirty; log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
}); });
} }

12
application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java

@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder() var newModelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key-updated")) .providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key-updated")
.build())
.modelId("o4-mini") .modelId("o4-mini")
.temperature(0.2) .temperature(0.2)
.topP(0.4) .topP(0.4)
@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest {
.tenantId(tenantId) .tenantId(tenantId)
.name("Test model 1") .name("Test model 1")
.configuration(OpenAiChatModelConfig.builder() .configuration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("o3-pro") .modelId("o3-pro")
.build()) .build())
.build(), AiModel.class); .build(), AiModel.class);
@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest {
private AiModel constructValidOpenAiModel(String name) { private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder() var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o") .modelId("gpt-4o")
.temperature(0.5) .temperature(0.5)
.topP(0.3) .topP(0.3)

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

@ -29,15 +29,23 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List;
import java.util.Map; import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
@DaoSqlTest @DaoSqlTest
public class CalculatedFieldControllerTest extends AbstractControllerTest { public class CalculatedFieldControllerTest extends AbstractControllerTest {
@ -84,7 +92,35 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF");
CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName());
assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1);
doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testSaveGeofencingCalculatedField() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig());
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
assertThat(savedCalculatedField).isNotNull();
assertThat(savedCalculatedField.getId()).isNotNull();
assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0);
assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId());
assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType());
assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig());
assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L);
savedCalculatedField.setName("Test CF"); savedCalculatedField.setName("Test CF");
@ -128,17 +164,41 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest {
} }
private CalculatedField getCalculatedField(DeviceId deviceId) { private CalculatedField getCalculatedField(DeviceId deviceId) {
return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig());
}
private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) {
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(deviceId); calculatedField.setEntityId(deviceId);
calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field"); calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1); calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig()); calculatedField.setConfiguration(configuration);
calculatedField.setVersion(1L); calculatedField.setVersion(1L);
return calculatedField; return calculatedField;
} }
private CalculatedFieldConfiguration getCalculatedFieldConfig() { private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() {
var config = new GeofencingCalculatedFieldConfiguration();
var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea")));
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration);
Output output = new Output();
output.setType(OutputType.TIME_SERIES);
config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude"));
config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration));
config.setScheduledUpdateEnabled(false);
config.setOutput(output);
return config;
}
private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument(); Argument argument = new Argument();

61
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
} }
@Test
public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> temps = new ArrayList<>();
List<String> deviceNames = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
device = doPost("/api/device", device, Device.class);
devices.add(device);
deviceNames.add(device.getName());
int temp = i * 10;
temps.add(String.valueOf(temp));
JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content)
.andExpect(status().isOk());
Thread.sleep(1);
}
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
String type = "device alarm" + i;
alarm.setType(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time")));
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
List<EntityKey> entityFields = new ArrayList<>();
entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
List<EntityKey> latestValues = new ArrayList<>();
latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature"));
EntityTypeFilter deviceTypeFilter = new EntityTypeFilter();
deviceTypeFilter.setEntityType(EntityType.DEVICE);
AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields);
PageData<AlarmData> alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10);
List<String> retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList();
assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps);
List<String> retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList();
assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames);
}
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception { private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery(); AlarmCountQuery countQuery = new AlarmCountQuery();

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

@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -321,18 +322,15 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues); Assert.assertNotNull(referenceValues);
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() { var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNotNull(widgetTypeInfos); Assert.assertNotNull(widgetTypeInfos);
Assert.assertFalse(widgetTypeInfos.isEmpty()); Assert.assertFalse(widgetTypeInfos.isEmpty());
Assert.assertEquals(1, widgetTypeInfos.size()); Assert.assertEquals(1, widgetTypeInfos.size());
var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0);
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(widgetTypeInfo);
Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo);
WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class);
Assert.assertNotNull(foundedWidgetType);
Assert.assertEquals(foundedWidgetType, dashboardInfo);
} }
@Test @Test
@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess); Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<WidgetTypeInfo>>>() { var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNull(widgetTypeInfos); Assert.assertNull(widgetTypeInfos);
} }
@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
Assert.assertNotNull(referenceValues); Assert.assertNotNull(referenceValues);
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() { var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNotNull(dashboardInfos); Assert.assertNotNull(dashboardInfos);
Assert.assertFalse(dashboardInfos.isEmpty()); Assert.assertFalse(dashboardInfos.isEmpty());
@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0);
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(dashboardInfo);
Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class);
Assert.assertNotNull(foundDashboard);
Assert.assertEquals(foundDashboard, dashboardInfo);
} }
@Test @Test
@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertTrue(isSuccess); Assert.assertTrue(isSuccess);
var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references");
var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<DashboardInfo>>>() { var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference<HashMap<String, List<EntityInfo>>>() {
}); });
Assert.assertNull(dashboardInfos); Assert.assertNull(dashboardInfos);
} }

50
application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java

@ -20,9 +20,12 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoUnit.MONTHS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@DaoSqlTest @DaoSqlTest
@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
private TenantId tenantId; private TenantId tenantId;
private Tenant savedTenant; private Tenant savedTenant;
private TenantProfile savedTenantProfile;
private static final int MAX_ENABLE_VALUE = 5000; private static final int MAX_ENABLE_VALUE = 5000;
private static final long VALUE_WARNING = 4500L; private static final long VALUE_WARNING = 4500L;
@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
loginSysAdmin(); loginSysAdmin();
TenantProfile tenantProfile = createTenantProfile(); TenantProfile tenantProfile = createTenantProfile();
TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
Assert.assertNotNull(savedTenantProfile); Assert.assertNotNull(savedTenantProfile);
Tenant tenant = new Tenant(); Tenant tenant = new Tenant();
@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest {
assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState());
} }
@Test
public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception {
ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID()));
apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED);
apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setReExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTransportState(ApiUsageStateValue.ENABLED);
apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED);
apiUsageState.setTenantId(tenantId);
apiUsageState.setEntityId(tenantId);
long now = System.currentTimeMillis();
long currentCycleTs = now - TimeUnit.DAYS.toMillis(30);
long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago
TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState);
tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs);
Map<EntityId, BaseApiUsageState> map = new HashMap<>();
map.put(tenantId, tenantApiUsageState);
Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates");
fieldToSet.setAccessible(true);
fieldToSet.set(service, map);
service.checkStartOfNextCycle();
long firstOfNextMonth = LocalDate.now()
.with((temporal) -> temporal.with(DAY_OF_MONTH, 1)
.plus(1, MONTHS))
.atStartOfDay(UTC).toInstant().toEpochMilli();
assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth);
}
private TenantProfile createTenantProfile() { private TenantProfile createTenantProfile() {
TenantProfile tenantProfile = new TenantProfile(); TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Tenant Profile"); tenantProfile.setName("Tenant Profile");

23
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy;
@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
@ -263,7 +264,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT") .put("allowedZonesEvent", "LEFT")
.put("allowedZonesStatus", "OUTSIDE") .put("allowedZonesStatus", "OUTSIDE")
@ -335,7 +336,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "LEFT") .put("allowedZonesEvent", "LEFT")
.put("restrictedZonesEvent", "ENTERED") .put("restrictedZonesEvent", "ENTERED")
@ -407,7 +408,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result2).isNotNull(); assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getType()).isEqualTo(output.getType());
assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getScope()).isEqualTo(output.getScope());
assertThat(result2.getResult()).isEqualTo( assertThat(result2.getResult().get("values")).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesStatus", "OUTSIDE") .put("allowedZonesStatus", "OUTSIDE")
.put("restrictedZonesStatus", "INSIDE") .put("restrictedZonesStatus", "INSIDE")
@ -459,21 +460,15 @@ public class GeofencingCalculatedFieldStateTest {
config.setEntityCoordinates(entityCoordinates); config.setEntityCoordinates(entityCoordinates);
ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone")));
allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone");
allowedZoneDynamicSourceConfiguration.setMaxLevel(1);
allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration);
allowedZonesGroup.setRelationType("CurrentZone"); allowedZonesGroup.setRelationType("CurrentZone");
allowedZonesGroup.setDirection(EntitySearchDirection.TO); allowedZonesGroup.setDirection(EntitySearchDirection.TO);
ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true);
var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration();
restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone")));
restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone");
restrictedZoneDynamicSourceConfiguration.setMaxLevel(1);
restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true);
restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration);
restrictedZonesGroup.setRelationType("CurrentZone"); restrictedZonesGroup.setRelationType("CurrentZone");
restrictedZonesGroup.setDirection(EntitySearchDirection.TO); restrictedZonesGroup.setDirection(EntitySearchDirection.TO);

3
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -129,7 +129,8 @@ public class SimpleCalculatedFieldStateTest {
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.update(newArgs, ctx)) assertThatThrownBy(() -> state.update(newArgs, ctx))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields."); .hasMessage("Unsupported argument type detected for argument: key3. " +
"Rolling argument entry is not supported for simple calculated fields.");
} }
@Test @Test

2
application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java

@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest {
private static AiModelConfig constructValidOpenAiModelConfig() { private static AiModelConfig constructValidOpenAiModelConfig() {
return OpenAiChatModelConfig.builder() return OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key")) .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build())
.modelId("gpt-4o") .modelId("gpt-4o")
.temperature(0.5) .temperature(0.5)
.topP(0.3) .topP(0.3)

83
application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java

@ -0,0 +1,83 @@
/**
* 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.resource;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.GeneralFileDescriptor;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.resource.TbResourceDataCache;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@DaoSqlTest
public class DefaultResourceDataCacheTest extends AbstractControllerTest {
@MockitoSpyBean
private ResourceService resourceService;
@Autowired
private TbResourceService tbResourceService;
@MockitoSpyBean
private TbResourceDataCache resourceDataCache;
@Test
public void testGetCachedResourceData() throws Exception {
loginTenantAdmin();
TbResource resource = new TbResource();
resource.setTenantId(tenantId);
resource.setTitle("File for AI request");
resource.setResourceType(ResourceType.GENERAL);
resource.setFileName("myTestJson.json");
GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json");
resource.setDescriptorValue(descriptor);
byte[] data = "This is a test prompt for AI request.".getBytes();
resource.setData(data);
TbResourceInfo savedResource = tbResourceService.save(resource);
verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData.getData()).isEqualTo(data);
assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor);
verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId());
// retrieve resource data second time
clearInvocations(resourceService);
TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedData2.getData()).isEqualTo(data);
verifyNoMoreInteractions(resourceService);
// delete resource, check cache
TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId());
tbResourceService.delete(resourceById, true, null);
verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId());
TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get();
assertThat(cachedDataAfterDeletion).isEqualTo(null);
}
}

125
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -24,8 +25,10 @@ import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.ai.TbAiNode;
import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration;
import org.thingsboard.rule.engine.ai.TbResponseFormat;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
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.data.rule.RuleNode;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.service.resource.TbResourceService; import org.thingsboard.server.service.resource.TbResourceService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
private WidgetTypeService widgetTypeService; private WidgetTypeService widgetTypeService;
@Autowired @Autowired
private DashboardService dashboardService; private DashboardService dashboardService;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private AiModelService aiModelService;
private Tenant savedTenant; private Tenant savedTenant;
private User tenantAdmin; private User tenantAdmin;
@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertFalse(result.getReferences().isEmpty()); Assert.assertFalse(result.getReferences().isEmpty());
Assert.assertEquals(1, result.getReferences().size()); Assert.assertEquals(1, result.getReferences().size());
WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0);
WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType);
Assert.assertNotNull(widgetTypeInfo); Assert.assertNotNull(widgetTypeInfo);
Assert.assertNotNull(foundWidgetTypeInfo); Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName()));
Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo);
TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource); Assert.assertNotNull(foundResource);
@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNotNull(result.getReferences()); Assert.assertNotNull(result.getReferences());
Assert.assertEquals(1, result.getReferences().size()); Assert.assertEquals(1, result.getReferences().size());
DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0);
DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId());
Assert.assertNotNull(dashboardInfo); Assert.assertNotNull(dashboardInfo);
Assert.assertNotNull(foundDashboardInfo); Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo);
Assert.assertEquals(foundDashboardInfo, dashboardInfo);
foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
Assert.assertNotNull(foundResource); Assert.assertNotNull(foundResource);
@ -598,6 +615,94 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
Assert.assertNull(foundResource); Assert.assertNull(foundResource);
} }
@Test
public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.GENERAL);
resource.setTitle("My resource");
resource.setFileName("test.json");
resource.setTenantId(savedTenant.getId());
resource.setData("<test></test>".getBytes());
TbResourceInfo savedResource = tbResourceService.save(resource);
RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId());
TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null);
assertThat(result).isNotNull();
assertThat(result.isSuccess()).isFalse();
assertThat(result.getReferences()).isNotEmpty().hasSize(1);
EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0);
assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test"));
TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(foundResource).isNotNull();
// force delete
TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null);
assertThat(deleteResult).isNotNull();
assertThat(deleteResult.isSuccess()).isTrue();
TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId());
assertThat(resourceAfterDeletion).isNull();
}
private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) {
AiModel model = constructValidOpenAiModel("Test model");
AiModel saved = aiModelService.save(model);
RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId);
ruleChain.setName("Test");
ruleChain.setType(RuleChainType.CORE);
ruleChain.setDebugMode(true);
ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b")));
ruleChain = ruleChainService.saveRuleChain(ruleChain);
RuleChainId ruleChainId = ruleChain.getId();
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChainId);
RuleNode aiNode = new RuleNode();
aiNode.setName("Ai request");
aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName());
aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
aiNode.setDebugSettings(DebugSettings.all());
TbAiNodeConfiguration configuration = new TbAiNodeConfiguration();
configuration.setResourceIds(Set.of(resourceId.getId()));
configuration.setModelId(saved.getId());
configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat());
configuration.setTimeoutSeconds(1);
configuration.setUserPrompt("What is temp");
aiNode.setConfiguration(JacksonUtil.valueToTree(configuration));
metaData.setNodes(Arrays.asList(aiNode));
metaData.setFirstNodeIndex(0);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId);
}
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(OpenAiProviderConfig.builder()
.baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL)
.apiKey("test-api-key")
.build())
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
return AiModel.builder()
.tenantId(tenantId)
.name(name)
.configuration(modelConfig)
.build();
}
@Test @Test
public void testFindTenantResourcesByTenantId() throws Exception { public void testFindTenantResourcesByTenantId() throws Exception {
loginSysAdmin(); loginSysAdmin();

3
common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java

@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
@ -83,6 +84,8 @@ public interface RelationService {
List<EntityRelation> findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit); List<EntityRelation> findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit);
ListenableFuture<List<EntityRelation>> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
// TODO: This method may be useful for some validations in the future // TODO: This method may be useful for some validations in the future
// ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to); // ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService {
byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); byte[] getResourceData(TenantId tenantId, TbResourceId resourceId);
TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
ResourceExportData exportResource(TbResourceInfo resourceInfo); ResourceExportData exportResource(TbResourceInfo resourceInfo);
List<ResourceExportData> exportResources(TenantId tenantId, Collection<TbResourceInfo> resources); List<ResourceExportData> exportResources(TenantId tenantId, Collection<TbResourceInfo> resources);
@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService {
TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data); TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data);
List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds);
} }

21
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java → common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java

@ -13,23 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.actors.calculatedField; package org.thingsboard.server.dao.resource;
import lombok.Data; import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@Data public interface TbResourceDataCache {
public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId; FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId);
private final CalculatedFieldId cfId;
@Override
public MsgType getMsgType() {
return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG;
}
void evictResourceData(TenantId tenantId, TbResourceId resourceId);
} }

29
common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class GeneralFileDescriptor {
private String mediaType;
}

3
common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java

@ -25,7 +25,8 @@ public enum ResourceType {
PKCS_12("application/x-pkcs12", false, false), PKCS_12("application/x-pkcs12", false, false),
JS_MODULE("application/javascript", true, true), JS_MODULE("application/javascript", true, true),
IMAGE(null, true, true), IMAGE(null, true, true),
DASHBOARD("application/json", true, true); DASHBOARD("application/json", true, true),
GENERAL(null, false, true);
@Getter @Getter
private final String mediaType; private final String mediaType;

6
common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.JsonSetter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo {
.orElse(null); .orElse(null);
} }
@JsonIgnore
public TbResourceDataInfo toResourceDataInfo() {
return new TbResourceDataInfo(data, getDescriptor());
}
@Override @Override
public String toString() { public String toString() {
return super.toString(); return super.toString();

31
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbResourceDataInfo {
private byte[] data;
private JsonNode descriptor;
}

3
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java

@ -17,7 +17,6 @@ package org.thingsboard.server.common.data;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.HasId;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -27,6 +26,6 @@ import java.util.Map;
public class TbResourceDeleteResult { public class TbResourceDeleteResult {
private boolean success; private boolean success;
private Map<String, List<? extends HasId<?>>> references; private Map<String, List<EntityInfo>> references;
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java

@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
property = "status", property = "status",
include = JsonTypeInfo.As.PROPERTY, include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true visible = true
) )
@JsonSubTypes({ @JsonSubTypes({
@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes
} }
record Failure( record Failure(
@Schema( @Schema(description = "A string containing details about the failure")
description = "A string containing details about the failure"
)
String errorDetails String errorDetails
) implements TbChatResponse { ) implements TbChatResponse {

8
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo( @JsonTypeInfo(
@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA")
}) })
public interface AiModelConfig { public interface AiModelConfig {
@ -69,7 +72,8 @@ public interface AiModelConfig {
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA")
}) })
AiProviderConfig providerConfig(); AiProviderConfig providerConfig();

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java

@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extend
permits permits
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer); ChatModel configure(Langchain4jChatModelConfigurer configurer);

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig(
@NotBlank String modelId, @NotBlank String modelId,
@PositiveOrZero Double temperature, @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> { ) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java

@ -34,7 +34,7 @@ public record AnthropicChatModelConfig(
@PositiveOrZero Double temperature, @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModelConfig> { ) implements AiChatModelConfig<AnthropicChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java

@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModelConfig> { ) implements AiChatModelConfig<AzureOpenAiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java

@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModelConfig> { ) implements AiChatModelConfig<GitHubModelsChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java

@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig(
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> { ) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java

@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig(
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> { ) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java

@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer {
ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig);
ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig);
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java

@ -35,7 +35,7 @@ public record MistralAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModelConfig> { ) implements AiChatModelConfig<MistralAiChatModelConfig> {

58
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai.model.chat;
import dev.langchain4j.model.chat.ChatModel;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.Builder;
import lombok.With;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
@Builder
public record OllamaChatModelConfig(
@NotNull @Valid OllamaProviderConfig providerConfig,
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@PositiveOrZero Integer topK,
Integer contextLength,
Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OllamaChatModelConfig> {
@Override
public AiProvider provider() {
return AiProvider.OLLAMA;
}
@Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this);
}
@Override
public boolean supportsJsonMode() {
return true;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java

@ -35,7 +35,7 @@ public record OpenAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModelConfig> { ) implements AiChatModelConfig<OpenAiChatModelConfig> {

3
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java

@ -24,6 +24,7 @@ public enum AiProvider {
MISTRAL_AI, MISTRAL_AI,
ANTHROPIC, ANTHROPIC,
AMAZON_BEDROCK, AMAZON_BEDROCK,
GITHUB_MODELS GITHUB_MODELS,
OLLAMA
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java

@ -19,4 +19,4 @@ public sealed interface AiProviderConfig
permits permits
OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig,
GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig,
AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {}

48
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.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.common.data.ai.provider;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
public record OllamaProviderConfig(
@NotNull String baseUrl,
@NotNull @Valid OllamaAuth auth
) implements AiProviderConfig {
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"),
@JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"),
@JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN")
})
public sealed interface OllamaAuth {
record None() implements OllamaAuth {}
record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {}
record Token(@NotNull String token) implements OllamaAuth {}
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java

@ -15,8 +15,32 @@
*/ */
package org.thingsboard.server.common.data.ai.provider; package org.thingsboard.server.common.data.ai.provider;
import jakarta.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import lombok.Builder;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;
@Builder
public record OpenAiProviderConfig( public record OpenAiProviderConfig(
@NotNull String apiKey String baseUrl,
) implements AiProviderConfig {} String apiKey
) implements AiProviderConfig {
public static final String OPENAI_OFFICIAL_BASE_URL = "https://api.openai.com/v1";
public OpenAiProviderConfig {
baseUrl = Objects.requireNonNullElse(baseUrl, OPENAI_OFFICIAL_BASE_URL);
}
@JsonIgnore
@AssertTrue(message = "API key is required when using the official OpenAI API")
public boolean isValid() {
if (baseUrl.equals(OPENAI_OFFICIAL_BASE_URL)) {
return StringUtils.isNotBlank(apiKey);
}
return true;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java

@ -38,7 +38,7 @@ public class Argument {
} }
public boolean hasRelationQuerySource() { public boolean hasRelationQuerySource() {
return hasDynamicSource() && CFArgumentDynamicSourceType.RELATION_QUERY.equals(refDynamicSourceConfiguration.getType()); return hasDynamicSource() && refDynamicSourceConfiguration.getType() == CFArgumentDynamicSourceType.RELATION_PATH_QUERY;
} }
public boolean hasOwnerSource() { public boolean hasOwnerSource() {

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java

@ -18,6 +18,6 @@ package org.thingsboard.server.common.data.cf.configuration;
public enum CFArgumentDynamicSourceType { public enum CFArgumentDynamicSourceType {
CURRENT_OWNER, CURRENT_OWNER,
RELATION_QUERY RELATION_PATH_QUERY
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java

@ -26,7 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
property = "type" property = "type"
) )
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY"),
@JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER") @JsonSubTypes.Type(value = CurrentOwnerDynamicSourceConfiguration.class, name = "CURRENT_OWNER")
}) })
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

71
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java

@ -0,0 +1,71 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import java.util.List;
import java.util.NoSuchElementException;
@Data
public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration {
private List<RelationPathLevel> levels;
@Override
public CFArgumentDynamicSourceType getType() {
return CFArgumentDynamicSourceType.RELATION_PATH_QUERY;
}
@Override
public void validate() {
if (CollectionsUtil.isEmpty(levels)) {
throw new IllegalArgumentException("At least one relation level must be specified!");
}
levels.forEach(RelationPathLevel::validate);
}
public List<EntityId> resolveEntityIds(List<EntityRelation> relations) {
EntitySearchDirection lastLevelDirection = getLastLevel().direction();
return switch (lastLevelDirection) {
case FROM -> relations.stream().map(EntityRelation::getTo).toList();
case TO -> relations.stream().map(EntityRelation::getFrom).toList();
};
}
public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
if (levels.size() > maxAllowedRelationLevel) {
throw new IllegalArgumentException("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
}
}
public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) {
return new EntityRelationPathQuery(entityId, levels);
}
private RelationPathLevel getLastLevel() {
return levels.get(levels.size() - 1);
}
}

86
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java

@ -1,86 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
import java.util.List;
@Data
public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration {
private int maxLevel;
private boolean fetchLastLevelOnly;
private EntitySearchDirection direction;
private String relationType;
@Override
public CFArgumentDynamicSourceType getType() {
return CFArgumentDynamicSourceType.RELATION_QUERY;
}
@Override
public void validate() {
if (maxLevel < 1) {
throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!");
}
if (direction == null) {
throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!");
}
if (StringUtils.isBlank(relationType)) {
throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!");
}
}
@JsonIgnore
public boolean isSimpleRelation() {
return maxLevel == 1;
}
public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) {
if (maxLevel > maxAllowedRelationLevel) {
throw new IllegalArgumentException("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName);
}
}
public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) {
if (isSimpleRelation()) {
throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!");
}
var entityRelationsQuery = new EntityRelationsQuery();
entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly));
entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList())));
return entityRelationsQuery;
}
public List<EntityId> resolveEntityIds(List<EntityRelation> relations) {
return switch (direction) {
case FROM -> relations.stream().map(EntityRelation::getTo).toList();
case TO -> relations.stream().map(EntityRelation::getFrom).toList();
};
}
}

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

@ -15,11 +15,8 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
@JsonIgnore
boolean isScheduledUpdateEnabled(); boolean isScheduledUpdateEnabled();
int getScheduledUpdateInterval(); int getScheduledUpdateInterval();

8
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java

@ -34,6 +34,8 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
private EntityCoordinates entityCoordinates; private EntityCoordinates entityCoordinates;
private Map<String, ZoneGroupConfiguration> zoneGroups; private Map<String, ZoneGroupConfiguration> zoneGroups;
private boolean scheduledUpdateEnabled;
private int scheduledUpdateInterval; private int scheduledUpdateInterval;
private Output output; private Output output;
@ -51,6 +53,7 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
return args; return args;
} }
@Override @Override
public List<EntityId> getReferencedEntities() { public List<EntityId> getReferencedEntities() {
return zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); return zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList();
@ -61,11 +64,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
return output; return output;
} }
@Override
public boolean isScheduledUpdateEnabled() {
return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasRelationQuerySource);
}
@Override @Override
public void validate() { public void validate() {
if (entityCoordinates == null) { if (entityCoordinates == null) {

2
common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java

@ -37,7 +37,7 @@ public class UserFields extends AbstractEntityFields {
@Override @Override
public String getName() { public String getName() {
return super.getEmail(); return getEmail();
} }
public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId,

3
common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData<MobileAppBundleId> implements HasT
private TenantId tenantId; private TenantId tenantId;
@Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @NotBlank
@NoXss
@Length(fieldName = "title") @Length(fieldName = "title")
private String title; private String title;
@Schema(description = "Application bundle description.") @Schema(description = "Application bundle description.")
@NoXss
@Length(fieldName = "description") @Length(fieldName = "description")
private String description; private String description;
@Schema(description = "Android application id") @Schema(description = "Android application id")

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java

@ -62,6 +62,7 @@ public class NotificationRule extends BaseData<NotificationRuleId> implements Ha
@Valid @Valid
private NotificationRuleRecipientsConfig recipientsConfig; private NotificationRuleRecipientsConfig recipientsConfig;
@Valid
private NotificationRuleConfig additionalConfig; private NotificationRuleConfig additionalConfig;
private NotificationRuleId externalId; private NotificationRuleId externalId;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java

@ -16,12 +16,14 @@
package org.thingsboard.server.common.data.notification.rule; package org.thingsboard.server.common.data.notification.rule;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable; import java.io.Serializable;
@Data @Data
public class NotificationRuleConfig implements Serializable { public class NotificationRuleConfig implements Serializable {
@NoXss
private String description; private String description;
} }

24
common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.relation;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
public record EntityRelationPathQuery(EntityId rootEntityId, List<RelationPathLevel> levels) {
}

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

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.relation;
import org.thingsboard.server.common.data.StringUtils;
public record RelationPathLevel(EntitySearchDirection direction, String relationType) {
public void validate() {
if (direction == null) {
throw new IllegalArgumentException("Direction must be specified!");
}
if (StringUtils.isBlank(relationType)) {
throw new IllegalArgumentException("Relation type must be specified!");
}
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -173,7 +173,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
@Schema(example = "10") @Schema(example = "10")
private long maxArgumentsPerCF = 10; private long maxArgumentsPerCF = 10;
@Schema(example = "3600") @Schema(example = "3600")
private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; private int minAllowedScheduledUpdateIntervalInSecForCF = 60;
@Schema(example = "10") @Schema(example = "10")
private int maxRelationLevelPerCfArgument = 10; private int maxRelationLevelPerCfArgument = 10;
@Builder.Default @Builder.Default

2
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java

@ -31,7 +31,7 @@ public class ArgumentTest {
@Test @Test
void validateWhenRelationQuerySourceConfigurationIsNotNull() { void validateWhenRelationQuerySourceConfigurationIsNotNull() {
var argument = new Argument(); var argument = new Argument();
argument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration());
assertThat(argument.hasDynamicSource()).isTrue(); assertThat(argument.hasDynamicSource()).isTrue();
assertThat(argument.hasRelationQuerySource()).isTrue(); assertThat(argument.hasRelationQuerySource()).isTrue();
assertThat(argument.hasOwnerSource()).isFalse(); assertThat(argument.hasOwnerSource()).isFalse();

126
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java

@ -0,0 +1,126 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class RelationPathQueryDynamicSourceConfigurationTest {
@Test
void typeShouldBeRelationQuery() {
var cfg = new RelationPathQueryDynamicSourceConfiguration();
assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY);
}
@ParameterizedTest
@NullAndEmptySource
void validateShouldThrowWhenLevelsIsNull(List<RelationPathLevel> levels) {
var cfg = new RelationPathQueryDynamicSourceConfiguration();
cfg.setLevels(levels);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("At least one relation level must be specified!");
}
@Test
void validateShouldCallValidateForPathLevels() {
List<RelationPathLevel> levels = new ArrayList<>();
RelationPathLevel lvl1 = mock(RelationPathLevel.class);
RelationPathLevel lvl2 = mock(RelationPathLevel.class);
levels.add(lvl1);
levels.add(lvl2);
var cfg = new RelationPathQueryDynamicSourceConfiguration();
cfg.setLevels(levels);
assertThatCode(cfg::validate).doesNotThrowAnyException();
verify(lvl1).validate();
verify(lvl2).validate();
}
@Test
void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() {
List<RelationPathLevel> levels = new ArrayList<>();
RelationPathLevel lvl1 = mock(RelationPathLevel.class);
RelationPathLevel lvl2 = mock(RelationPathLevel.class);
levels.add(lvl1);
levels.add(lvl2);
when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM);
EntityRelation rel1 = mock(EntityRelation.class);
EntityRelation rel2 = mock(EntityRelation.class);
when(rel1.getTo()).thenReturn(mock(EntityId.class));
when(rel2.getTo()).thenReturn(mock(EntityId.class));
var cfg = new RelationPathQueryDynamicSourceConfiguration();
cfg.setLevels(levels);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getTo(), rel2.getTo());
}
@Test
void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() {
List<RelationPathLevel> levels = new ArrayList<>();
RelationPathLevel lvl1 = mock(RelationPathLevel.class);
RelationPathLevel lvl2 = mock(RelationPathLevel.class);
levels.add(lvl1);
levels.add(lvl2);
when(lvl2.direction()).thenReturn(EntitySearchDirection.TO);
EntityRelation rel1 = mock(EntityRelation.class);
EntityRelation rel2 = mock(EntityRelation.class);
when(rel1.getFrom()).thenReturn(mock(EntityId.class));
when(rel2.getFrom()).thenReturn(mock(EntityId.class));
var cfg = new RelationPathQueryDynamicSourceConfiguration();
cfg.setLevels(levels);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom());
}
}

216
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java

@ -1,216 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class RelationQueryDynamicSourceConfigurationTest {
@Mock
EntityId rootEntityId;
@Mock
EntityRelation rel1;
@Mock
EntityRelation rel2;
@Test
void typeShouldBeRelationQuery() {
var cfg = new RelationQueryDynamicSourceConfiguration();
assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY);
}
@Test
void validateShouldThrowWhenMaxLevelLessThanOne() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(0);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!");
}
@Test
void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() {
int maxAllowedRelationLevel = 2;
int argumentMaxRelationLevel = 3;
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(argumentMaxRelationLevel);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
String testRelationArgument = "testRelationArgument";
assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Max relation level is greater than configured " +
"maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument);
}
@Test
void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() {
int maxAllowedRelationLevel = 5;
int argumentMaxRelationLevel = 2;
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(argumentMaxRelationLevel);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
String testRelationArgument = "testRelationArgument";
assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException();
}
@Test
void validateShouldThrowWhenDirectionIsNull() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setDirection(null);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration direction must be specified!");
}
@ParameterizedTest
@ValueSource(strings = {" "})
@NullAndEmptySource
void validateShouldThrowWhenRelationTypeIsNull(String relationType) {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setDirection(EntitySearchDirection.TO);
cfg.setRelationType(relationType);
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Relation query dynamic source configuration relation type must be specified!");
}
@ParameterizedTest
@NullAndEmptySource
void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List<EntityType> entityTypes) {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
assertThat(cfg.isSimpleRelation()).isTrue();
}
@Test
void isSimpleRelationFalseWhenMaxLevelNotOne() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
assertThat(cfg.isSimpleRelation()).isFalse();
}
@ParameterizedTest
@NullAndEmptySource
void toEntityRelationsQueryShouldThrowForSimpleRelation(List<EntityType> entityTypes) {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(1);
cfg.setFetchLastLevelOnly(false);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Entity relations query can't be created for a simple relation!");
}
@Test
void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
cfg.setFetchLastLevelOnly(true);
cfg.setDirection(EntitySearchDirection.TO);
cfg.setRelationType(EntityRelation.MANAGES_TYPE);
var query = cfg.toEntityRelationsQuery(rootEntityId);
assertThat(query).isNotNull();
RelationsSearchParameters params = query.getParameters();
assertThat(params).isNotNull();
assertThat(params.getRootId()).isEqualTo(rootEntityId.getId());
assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO);
assertThat(params.getMaxLevel()).isEqualTo(2);
assertThat(params.isFetchLastLevelOnly()).isTrue();
assertThat(query.getFilters()).hasSize(1);
assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class);
RelationEntityTypeFilter filter = query.getFilters().get(0);
assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE);
}
@Test
void resolveEntityIdsFromDirectionFROMReturnsToIds() {
when(rel1.getTo()).thenReturn(mock(EntityId.class));
when(rel2.getTo()).thenReturn(mock(EntityId.class));
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setDirection(EntitySearchDirection.FROM);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getTo(), rel2.getTo());
}
@Test
void resolveEntityIdsFromDirectionTOReturnsFromIds() {
when(rel1.getFrom()).thenReturn(mock(EntityId.class));
when(rel2.getFrom()).thenReturn(mock(EntityId.class));
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setDirection(EntitySearchDirection.TO);
var out = cfg.resolveEntityIds(List.of(rel1, rel2));
assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom());
}
@Test
void validateShouldPassForValidConfig() {
var cfg = new RelationQueryDynamicSourceConfiguration();
cfg.setMaxLevel(2);
cfg.setFetchLastLevelOnly(false);
cfg.setDirection(EntitySearchDirection.FROM);
cfg.setRelationType(EntityRelation.CONTAINS_TYPE);
assertThatCode(cfg::validate).doesNotThrowAnyException();
}
}

28
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java

@ -31,7 +31,6 @@ import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
@ -99,33 +98,6 @@ public class GeofencingCalculatedFieldConfigurationTest {
verify(zoneGroupConfigurationB).validate(zoneGroupBName); verify(zoneGroupConfigurationB).validate(zoneGroupBName);
} }
@Test
void scheduledUpdateDisabledWhenIntervalIsZero() {
var cfg = new GeofencingCalculatedFieldConfiguration();
cfg.setScheduledUpdateInterval(0);
assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
}
@Test
void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() {
var cfg = new GeofencingCalculatedFieldConfiguration();
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
when(zoneGroupConfigurationMock.hasRelationQuerySource()).thenReturn(false);
cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
cfg.setScheduledUpdateInterval(60);
assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
}
@Test
void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() {
var cfg = new GeofencingCalculatedFieldConfiguration();
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
when(zoneGroupConfigurationMock.hasRelationQuerySource()).thenReturn(true);
cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock));
cfg.setScheduledUpdateInterval(60);
assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
}
@Test @Test
void testGetArgumentsOverride() { void testGetArgumentsOverride() {
var cfg = new GeofencingCalculatedFieldConfiguration(); var cfg = new GeofencingCalculatedFieldConfiguration();

4
common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@ -101,7 +101,7 @@ public class ZoneGroupConfigurationTest {
@Test @Test
void whenHasRelationQuerySourceCalled_shouldReturnTrueIfRelationQuerySourceConfigurationIsNotNull() { void whenHasRelationQuerySourceCalled_shouldReturnTrueIfRelationQuerySourceConfigurationIsNotNull() {
var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration());
assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isTrue(); assertThat(zoneGroupConfiguration.hasRelationQuerySource()).isTrue();
} }

2
common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java

@ -152,8 +152,6 @@ public enum MsgType {
CF_ENTITY_DELETE_MSG, CF_ENTITY_DELETE_MSG,
CF_ARGUMENT_RESET_MSG, // Sent to reset argument; CF_ARGUMENT_RESET_MSG, // Sent to reset argument;
CF_DYNAMIC_ARGUMENTS_REFRESH_MSG,
CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG,
CF_REEVALUATE_MSG; CF_REEVALUATE_MSG;
@Getter @Getter

12
common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java

@ -60,16 +60,4 @@ public class SchedulerUtils {
return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
} }
public static long getStartOfNextNextMonth() {
return getStartOfNextNextMonth(UTC);
}
public static long getStartOfNextNextMonth(ZoneId zoneId) {
return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli();
}
public static TemporalAdjuster firstDayOfNextNextMonth() {
return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS);
}
} }

15
common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java

@ -15,12 +15,15 @@
*/ */
package org.thingsboard.common.util; package org.thingsboard.common.util;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -71,4 +74,16 @@ public class DonAsynchron {
return future; return future;
} }
public static <T> FluentFuture<T> toFluentFuture(CompletableFuture<T> completable) {
SettableFuture<T> future = SettableFuture.create();
completable.whenComplete((result, exception) -> {
if (exception != null) {
future.setException(exception);
} else {
future.set(result);
}
});
return FluentFuture.from(future);
}
} }

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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao; package org.thingsboard.server.dao;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -22,8 +23,8 @@ import java.util.List;
public interface ResourceContainerDao<T extends HasId<?>> { public interface ResourceContainerDao<T extends HasId<?>> {
List<T> findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); List<EntityInfo> findByTenantIdAndResource(TenantId tenantId, String reference, int limit);
List<T> findByResourceLink(String link, int limit); List<EntityInfo> findByResource(String reference, int limit);
} }

28
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -32,17 +32,21 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
@ -495,6 +499,30 @@ public class BaseRelationService implements RelationService {
return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit);
} }
@Override
public ListenableFuture<List<EntityRelation>> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) {
log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery);
validateId(tenantId, id -> "Invalid tenant id: " + id);
validate(relationPathQuery);
if (relationPathQuery.levels().size() == 1) {
RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0);
return switch (relationPathLevel.direction()) {
case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON);
};
}
return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery));
}
private void validate(EntityRelationPathQuery relationPathQuery) {
validateId((UUIDBased) relationPathQuery.rootEntityId(), id -> "Invalid root entity id: " + id);
List<RelationPathLevel> levels = relationPathQuery.levels();
if (CollectionUtils.isEmpty(levels)) {
throw new DataValidationException("Relation path levels should be specified!");
}
levels.forEach(RelationPathLevel::validate);
}
protected void validate(EntityRelation relation) { protected void validate(EntityRelation relation) {
if (relation == null) { if (relation == null) {
throw new DataValidationException("Relation type should be specified!"); throw new DataValidationException("Relation type should be specified!");

3
dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationPathQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
@ -71,4 +72,6 @@ public interface RelationDao {
List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); List<EntityRelation> findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit);
List<EntityRelation> findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery);
} }

5
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao;
import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.dashboard.DashboardInfoDao;
import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
import org.thingsboard.server.dao.util.ImageUtils; import org.thingsboard.server.dao.util.ImageUtils;
@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic
public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator,
AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao,
WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) {
super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao);
this.assetProfileDao = assetProfileDao; this.assetProfileDao = assetProfileDao;
this.deviceProfileDao = deviceProfileDao; this.deviceProfileDao = deviceProfileDao;
this.widgetsBundleDao = widgetsBundleDao; this.widgetsBundleDao = widgetsBundleDao;

72
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -35,11 +35,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey;
import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent;
import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceExportData;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceDeleteResult;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
@ -56,6 +58,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator;
@ -92,13 +95,16 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
protected final ResourceDataValidator resourceValidator; protected final ResourceDataValidator resourceValidator;
protected final WidgetTypeDao widgetTypeDao; protected final WidgetTypeDao widgetTypeDao;
protected final DashboardInfoDao dashboardInfoDao; protected final DashboardInfoDao dashboardInfoDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceContainerDaoMap = new HashMap<>(); protected final RuleChainDao ruleChainDao;
private final Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap = new HashMap<>();
private final Map<EntityType, ResourceContainerDao<?>> generalResourceContainerDaoMap = new HashMap<>();
protected static final int MAX_ENTITIES_TO_FIND = 10; protected static final int MAX_ENTITIES_TO_FIND = 10;
@PostConstruct @PostConstruct
public void init() { public void init() {
resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao);
resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao);
generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao);
} }
@Autowired @Lazy @Autowired @Lazy
@ -206,6 +212,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return resourceDao.getResourceData(tenantId, resourceId); return resourceDao.getResourceData(tenantId, resourceId);
} }
@Override
public TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing getResourceDataInfo [{}] [{}]", tenantId, resourceId);
return resourceDao.getResourceDataInfo(tenantId, resourceId);
}
@Override @Override
public ResourceExportData exportResource(TbResourceInfo resourceInfo) { public ResourceExportData exportResource(TbResourceInfo resourceInfo) {
byte[] data = getResourceData(resourceInfo.getTenantId(), resourceInfo.getId()); byte[] data = getResourceData(resourceInfo.getTenantId(), resourceInfo.getId());
@ -344,32 +356,48 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
} }
if (!force) { if (!force) {
if (resource.getResourceType() == ResourceType.JS_MODULE) { Map<String, List<EntityInfo>> references = findResourceReferences(tenantId, resource);
var link = resource.getLink(); if (!references.isEmpty()) {
Map<String, List<? extends HasId<?>>> affectedEntities = new HashMap<>(); success = false;
result.references(references);
resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> {
var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) :
resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND);
if (!entities.isEmpty()) {
affectedEntities.put(entityType.name(), entities);
}
});
if (!affectedEntities.isEmpty()) {
success = false;
result.references(affectedEntities);
}
} }
} }
if (success) { if (success) {
resourceDao.removeById(tenantId, resourceId.getId()); resourceDao.removeById(tenantId, resourceId.getId());
publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build());
} }
return result.success(success).build(); return result.success(success).build();
} }
private Map<String, List<EntityInfo>> findResourceReferences(TenantId tenantId, TbResourceInfo resource) {
Map<String, List<EntityInfo>> references = new HashMap<>();
if (resource.getResourceType() == ResourceType.JS_MODULE) {
var ref = resource.getLink();
findReferences(tenantId, references, ref, resourceLinkContainerDaoMap);
}
if (resource.getResourceType() == ResourceType.GENERAL) {
var ref = resource.getId().getId().toString();
findReferences(tenantId, references, ref, generalResourceContainerDaoMap);
}
return references;
}
private void findReferences(TenantId tenantId, Map<String, List<EntityInfo>> references, String ref, Map<EntityType, ResourceContainerDao<?>> resourceLinkContainerDaoMap) {
resourceLinkContainerDaoMap.forEach((entityType, dao) -> {
List<EntityInfo> entities = tenantId.isSysTenantId()
? dao.findByResource(ref, MAX_ENTITIES_TO_FIND)
: dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND);
if (!entities.isEmpty()) {
references.put(entityType.name(), entities);
}
});
}
@Override @Override
public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { public void deleteEntity(TenantId tenantId, EntityId id, boolean force) {
deleteResource(tenantId, (TbResourceId) id, force); deleteResource(tenantId, (TbResourceId) id, force);
@ -663,6 +691,12 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
return saveResource(resource); return saveResource(resource);
} }
@Override
public List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds) {
log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds);
return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds);
}
@Override @Override
public String calculateEtag(byte[] data) { public String calculateEtag(byte[] data) {
return Hashing.sha256().hashBytes(data).toString(); return Hashing.sha256().hashBytes(data).toString();

72
dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java

@ -0,0 +1,72 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.resource;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.util.concurrent.FluentFuture;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultTbResourceDataCache implements TbResourceDataCache {
private final ResourceService resourceService;
private final JpaExecutorService executorService;
@Value("${cache.tbResourceData.maxSize:100000}")
private int cacheMaxSize;
@Value("${cache.tbResourceData.timeToLiveInMinutes:44640}")
private int cacheValueTtl;
private AsyncLoadingCache<ResourceDataKey, TbResourceDataInfo> cache;
@PostConstruct
private void init() {
cache = Caffeine.newBuilder()
.maximumSize(cacheMaxSize)
.expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES)
.executor(executorService)
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor));
}
@Override
public FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) {
log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId);
return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId)));
}
@Override
public void evictResourceData(TenantId tenantId, TbResourceId resourceId) {
cache.asMap().remove(new ResourceDataKey(tenantId, resourceId));
log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId);
}
record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {}
}

2
dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceSubType;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceDataInfo;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao,
long getResourceSize(TenantId tenantId, TbResourceId resourceId); long getResourceSize(TenantId tenantId, TbResourceId resourceId);
TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId);
} }

2
dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey); TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey);
List<TbResourceInfo> findSystemOrTenantResourcesByIds(TenantId tenantId, List<TbResourceId> resourceIds);
} }

4
dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java

@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao; import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.ResourceContainerDao;
import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.TenantEntityDao;
import java.util.Collection; import java.util.Collection;
@ -31,7 +33,7 @@ import java.util.UUID;
/** /**
* Created by igor on 3/12/18. * Created by igor on 3/12/18.
*/ */
public interface RuleChainDao extends Dao<RuleChain>, TenantEntityDao<RuleChain>, ExportableEntityDao<RuleChainId, RuleChain> { public interface RuleChainDao extends Dao<RuleChain>, TenantEntityDao<RuleChain>, ExportableEntityDao<RuleChainId, RuleChain>, ResourceContainerDao<RuleChain> {
/** /**
* Find rule chains by tenantId and page link. * Find rule chains by tenantId and page link.

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

Loading…
Cancel
Save