Browse Source

UI merge and resolve conflict

pull/13371/head
Artem Dzhereleiko 12 months ago
parent
commit
21ffc12378
  1. 56
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 8
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  4. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 122
      application/src/main/java/org/thingsboard/server/controller/AiModelController.java
  6. 143
      application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java
  7. 22
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  8. 2
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  9. 35
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  10. 133
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  11. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java
  13. 9
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  14. 214
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  15. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  16. 7
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  17. 16
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  19. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  20. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  21. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  22. 14
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  23. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  24. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  25. 79
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java
  26. 169
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java
  27. 28
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java
  28. 1
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  29. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java
  30. 45
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  31. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java
  32. 36
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java
  33. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java
  34. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java
  35. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  36. 101
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  37. 2
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  38. 10
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  39. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  40. 8
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java
  41. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  42. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
  43. 49
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  44. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  45. 44
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java
  46. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  47. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
  48. 13
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
  49. 76
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java
  50. 12
      application/src/main/resources/thingsboard.yml
  51. 58
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  52. 13
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  53. 267
      application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java
  54. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  55. 4
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  56. 29
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  57. 80
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  58. 4
      common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java
  59. 7
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java
  60. 18
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
  61. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  62. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java
  63. 4
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  64. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  65. 8
      common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java
  66. 12
      common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java
  67. 3
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  68. 38
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  69. 4
      common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java
  70. 84
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java
  71. 60
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java
  72. 41
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java
  73. 15
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
  74. 21
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
  75. 23
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
  76. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
  77. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
  78. 27
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
  79. 27
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
  80. 16
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java
  81. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
  82. 25
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
  83. 9
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventType.java
  84. 16
      common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java
  85. 4
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  86. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java
  87. 12
      common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java
  88. 6
      common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
  89. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java
  90. 41
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java
  91. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java
  92. 40
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  93. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  94. 18
      common/edge-api/src/main/proto/edge.proto
  95. 19
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  96. 8
      common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java
  97. 10
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  98. 4
      common/proto/src/main/proto/queue.proto
  99. 5
      common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java
  100. 27
      common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java

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

@ -14,48 +14,24 @@
-- limitations under the License. -- limitations under the License.
-- --
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START -- UPDATE OTA PACKAGE EXTERNAL ID START
UPDATE tenant_profile ALTER TABLE ota_package
SET profile_data = jsonb_set( ADD COLUMN IF NOT EXISTS external_id uuid;
profile_data, ALTER TABLE ota_package
'{configuration}', ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
(
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration'
||
COALESCE(
CASE
WHEN profile_data -> 'configuration' ->
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration'
)
END,
'{}'::jsonb
)
)
)
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END -- UPDATE OTA PACKAGE EXTERNAL ID END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS START -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START
UPDATE notification_rule DROP INDEX IF EXISTS idx_device_external_id;
SET trigger_config = REGEXP_REPLACE( DROP INDEX IF EXISTS idx_device_profile_external_id;
trigger_config, DROP INDEX IF EXISTS idx_asset_external_id;
'"CASSANDRA_QUERIES"', DROP INDEX IF EXISTS idx_entity_view_external_id;
'"CASSANDRA_WRITE_QUERIES_CORE","CASSANDRA_READ_QUERIES_CORE","CASSANDRA_WRITE_QUERIES_RULE_ENGINE","CASSANDRA_READ_QUERIES_RULE_ENGINE","CASSANDRA_WRITE_QUERIES_MONOLITH","CASSANDRA_READ_QUERIES_MONOLITH"', DROP INDEX IF EXISTS idx_rule_chain_external_id;
'g' DROP INDEX IF EXISTS idx_dashboard_external_id;
) DROP INDEX IF EXISTS idx_customer_external_id;
WHERE trigger_type = 'RATE_LIMITS' DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END

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

@ -35,7 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -63,7 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -315,11 +315,11 @@ public class ActorSystemContext {
@Autowired @Autowired
@Getter @Getter
private RuleEngineAiModelService aiModelService; private RuleEngineAiChatModelService aiChatModelService;
@Autowired @Autowired
@Getter @Getter
private AiModelSettingsService aiModelSettingsService; private AiModelService aiModelService;
@Autowired @Autowired
@Getter @Getter

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

@ -68,7 +68,7 @@ import java.util.stream.Collectors;
*/ */
@Slf4j @Slf4j
public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor { public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareMsgProcessor {
// (1 for result persistence + 1 for the state persistence ) // (1 for result persistence + 1 for the state persistence)
public static final int CALLBACKS_PER_CF = 2; public static final int CALLBACKS_PER_CF = 2;
final TenantId tenantId; final TenantId tenantId;

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

@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService; import org.thingsboard.rule.engine.api.RuleEngineApiUsageStateService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache; import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
@ -77,7 +77,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.TbMsgProcessingStackItem;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -1027,13 +1027,13 @@ public class DefaultTbContext implements TbContext {
} }
@Override @Override
public RuleEngineAiModelService getAiModelService() { public RuleEngineAiChatModelService getAiChatModelService() {
return mainCtx.getAiModelService(); return mainCtx.getAiChatModelService();
} }
@Override @Override
public AiModelSettingsService getAiModelSettingsService() { public AiModelService getAiModelService() {
return mainCtx.getAiModelSettingsService(); return mainCtx.getAiModelService();
} }
@Override @Override

122
application/src/main/java/org/thingsboard/server/controller/AiModelController.java

@ -17,31 +17,141 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ChatRequest;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.dto.TbChatRequest; import org.thingsboard.server.common.data.ai.dto.TbChatRequest;
import org.thingsboard.server.common.data.ai.dto.TbChatResponse; import org.thingsboard.server.common.data.ai.dto.TbChatResponse;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.service.ai.AiModelService; import org.thingsboard.server.service.ai.AiChatModelService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.time.Duration; import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@Validated
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api/ai/model") @RequestMapping("/api/ai/model")
class AiModelController extends BaseController { class AiModelController extends BaseController {
private final AiModelService aiModelService; private final AiChatModelService aiChatModelService;
@ApiOperation(
value = "Create or update AI model (saveAiModel)",
notes = "Creates or updates an AI model record.\n\n" +
"• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new record and returns it in the `id` field of the response.\n\n" +
"• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" +
"Tenant ID for the AI model will be taken from the authenticated user making the request, regardless of any value provided in the request body." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping
public AiModel saveAiModel(@RequestBody @Valid AiModel model) throws ThingsboardException {
var user = getCurrentUser();
model.setTenantId(user.getTenantId());
checkEntity(model.getId(), model, Resource.AI_MODEL);
return tbAiModelService.save(model, user);
}
@ApiOperation(
value = "Get AI model by ID (getAiModelById)",
notes = "Fetches an AI model record by its `id`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/{modelUuid}")
public AiModel getAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
return checkAiModelId(new AiModelId(modelUuid), Operation.READ);
}
@ApiOperation(
value = "Get AI models (getAiModels)",
notes = "Returns a page of AI models. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping
public PageData<AiModel> getAiModels(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = AI_MODEL_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder
) throws ThingsboardException {
var user = getCurrentUser();
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.READ);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiModelService.findAiModelsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(
value = "Delete AI model by ID (deleteAiModelById)",
notes = "Deletes the AI model record by its `id`. " +
"If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " +
"If no such record exists, the endpoint returns `false`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/{modelUuid}")
public boolean deleteAiModelById(
@Parameter(
description = "ID of the AI model record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID modelUuid
) throws ThingsboardException {
var user = getCurrentUser();
var modelId = new AiModelId(modelUuid);
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE);
Optional<AiModel> toDelete = aiModelService.findAiModelByTenantIdAndId(user.getTenantId(), modelId);
if (toDelete.isEmpty()) {
return false;
}
accessControlService.checkPermission(user, Resource.AI_MODEL, Operation.DELETE, modelId, toDelete.get());
return tbAiModelService.delete(toDelete.get(), user);
}
@ApiOperation( @ApiOperation(
value = "Send request to AI chat model (sendChatRequest)", value = "Send request to AI chat model (sendChatRequest)",
@ -53,13 +163,13 @@ class AiModelController extends BaseController {
@PostMapping("/chat") @PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) { public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest(); ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest();
AiChatModel<?> chatModel = tbChatRequest.chatModel(); AiChatModelConfig<?> chatModelConfig = tbChatRequest.chatModelConfig();
ListenableFuture<TbChatResponse> future = aiModelService.sendChatRequestAsync(chatModel, langChainChatRequest) ListenableFuture<TbChatResponse> future = aiChatModelService.sendChatRequestAsync(chatModelConfig, langChainChatRequest)
.transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor()) .transform(chatResponse -> (TbChatResponse) new TbChatResponse.Success(chatResponse.aiMessage().text()), directExecutor())
.catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor()); .catching(Throwable.class, ex -> new TbChatResponse.Failure(ex.getMessage()), directExecutor());
Integer requestTimeoutSeconds = chatModel.modelConfig().timeoutSeconds(); Integer requestTimeoutSeconds = chatModelConfig.timeoutSeconds();
return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future); return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future);
} }

143
application/src/main/java/org/thingsboard/server/controller/AiModelSettingsController.java

@ -1,143 +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.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Optional;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
@Validated
@RestController
@RequestMapping("/api/ai/model/settings")
class AiModelSettingsController extends BaseController {
@ApiOperation(
value = "Create or update AI model settings (saveAiModelSettings)",
notes = "Creates or updates an AI model settings record.\n\n" +
"• **Create:** Omit the `id` to create a new record. The platform assigns a UUID to the new settings and returns it in the `id` field of the response.\n\n" +
"• **Update:** Include an existing `id` to modify that record. If no matching record exists, the API responds with **404 Not Found**.\n\n" +
"Tenant ID for the AI model settings will be taken from the authenticated user making the request, regardless of any value provided in the request body." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping
public AiModelSettings saveAiModelSettings(@RequestBody @Valid AiModelSettings settings) throws ThingsboardException {
var user = getCurrentUser();
settings.setTenantId(user.getTenantId());
checkEntity(settings.getId(), settings, Resource.AI_MODEL_SETTINGS);
return tbAiModelSettingsService.save(settings, user);
}
@ApiOperation(
value = "Get AI model settings by ID (getAiModelSettingsById)",
notes = "Fetches an AI model settings record by its `id`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/{settingsUuid}")
public AiModelSettings getAiModelSettingsById(
@Parameter(
description = "ID of the AI model settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID settingsUuid
) throws ThingsboardException {
return checkAiModelSettingsId(new AiModelSettingsId(settingsUuid), Operation.READ);
}
@ApiOperation(
value = "Get AI model settings (getAiModelSettings)",
notes = "Returns a page of AI model settings. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping
public PageData<AiModelSettings> getAiModelSettings(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "provider", "modelId"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder
) throws ThingsboardException {
var user = getCurrentUser();
accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.READ);
var pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return aiModelSettingsService.findAiModelSettingsByTenantId(user.getTenantId(), pageLink);
}
@ApiOperation(
value = "Delete AI model settings by ID (deleteAiModelSettingsById)",
notes = "Deletes the AI model settings record by its `id`. " +
"If a record with the specified `id` exists, the record is deleted and the endpoint returns `true`. " +
"If no such record exists, the endpoint returns `false`." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/{settingsUuid}")
public boolean deleteAiModelSettingsById(
@Parameter(
description = "ID of the AI model settings record",
required = true,
example = "de7900d4-30e2-11f0-9cd2-0242ac120002"
)
@PathVariable UUID settingsUuid
) throws ThingsboardException {
var user = getCurrentUser();
var settingsId = new AiModelSettingsId(settingsUuid);
accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE);
Optional<AiModelSettings> toDelete = aiModelSettingsService.findAiModelSettingsByTenantIdAndId(user.getTenantId(), settingsId);
if (toDelete.isEmpty()) {
return false;
}
accessControlService.checkPermission(user, Resource.AI_MODEL_SETTINGS, Operation.DELETE, settingsId, toDelete.get());
return tbAiModelSettingsService.delete(toDelete.get(), user);
}
}

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

@ -61,7 +61,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantInfo;
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.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -76,7 +76,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -131,7 +131,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction;
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.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -178,7 +178,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.entitiy.ai.TbAiModelSettingsService; import org.thingsboard.server.service.entitiy.ai.TbAiModelService;
import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService;
import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -383,10 +383,10 @@ public abstract class BaseController {
protected CalculatedFieldService calculatedFieldService; protected CalculatedFieldService calculatedFieldService;
@Autowired @Autowired
protected AiModelSettingsService aiModelSettingsService; protected AiModelService aiModelService;
@Autowired @Autowired
protected TbAiModelSettingsService tbAiModelSettingsService; protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}") @Value("${server.log_controller_error_stack_trace}")
@Getter @Getter
@ -400,7 +400,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) { public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e); ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause(); e = (Exception) thingsboardException.getCause();
} else { } else {
e = thingsboardException; e = thingsboardException;
@ -448,7 +448,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
@ -644,7 +644,7 @@ public abstract class BaseController {
case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation);
case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
case AI_MODEL_SETTINGS -> checkAiModelSettingsId(new AiModelSettingsId(entityId.getId()), operation); case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation);
default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}; };
} catch (Exception e) { } catch (Exception e) {
@ -848,8 +848,8 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation); return checkEntityId(jobId, jobService::findJobById, operation);
} }
AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException { AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation); return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation);
} }
protected <I extends EntityId> I emptyId(EntityType entityType) { protected <I extends EntityId> I emptyId(EntityType entityType) {

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

@ -90,7 +90,7 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String AI_MODEL_SETTINGS_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model settings name, provider and model ID."; protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";

35
application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java

@ -24,13 +24,14 @@ import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackage;
@ -49,8 +50,6 @@ import org.thingsboard.server.service.entitiy.ota.TbOtaPackageService;
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.io.IOException;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION;
@ -80,8 +79,7 @@ public class OtaPackageController extends BaseController {
@ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/{otaPackageId}/download")
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -105,8 +103,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " +
OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/info/{otaPackageId}")
@ResponseBody
public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -118,8 +115,7 @@ public class OtaPackageController extends BaseController {
notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + notes = "Fetch the OTA Package object based on the provided OTA Package Id. " +
"The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) @GetMapping(value = "/otaPackage/{otaPackageId}")
@ResponseBody
public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
@ -134,10 +130,9 @@ public class OtaPackageController extends BaseController {
"Referencing non-existing OTA Package Id will cause 'Not Found' error. " + "Referencing non-existing OTA Package Id will cause 'Not Found' error. " +
"\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH) "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage", method = RequestMethod.POST) @PostMapping(value = "/otaPackage")
@ResponseBody
public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.") public OtaPackageInfo saveOtaPackageInfo(@Parameter(description = "A JSON value representing the OTA Package.")
@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws Exception {
otaPackageInfo.setTenantId(getTenantId()); otaPackageInfo.setTenantId(getTenantId());
checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE); checkEntity(otaPackageInfo.getId(), otaPackageInfo, Resource.OTA_PACKAGE);
@ -148,8 +143,7 @@ public class OtaPackageController extends BaseController {
notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE))) requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId,
@Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'") @Parameter(description = "OTA Package checksum. For example, '0xd87f7e0c'")
@ -157,7 +151,7 @@ public class OtaPackageController extends BaseController {
@Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"})) @Parameter(description = "OTA Package checksum algorithm.", schema = @Schema(allowableValues = {"MD5", "SHA256", "SHA384", "SHA512", "CRC32", "MURMUR3_32", "MURMUR3_128"}))
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@Parameter(description = "OTA Package data.") @Parameter(description = "OTA Package data.")
@RequestPart MultipartFile file) throws ThingsboardException, IOException { @RequestPart MultipartFile file) throws Exception {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr);
OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId));
@ -172,8 +166,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " + notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages", method = RequestMethod.GET) @GetMapping(value = "/otaPackages")
@ResponseBody
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -192,8 +185,7 @@ public class OtaPackageController extends BaseController {
notes = "Returns a page of OTA Package Info objects owned by tenant. " + notes = "Returns a page of OTA Package Info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) @GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
@ResponseBody
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId, @PathVariable("deviceProfileId") String strDeviceProfileId,
@Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"})) @Parameter(description = "OTA Package type.", schema = @Schema(allowableValues = {"FIRMWARE", "SOFTWARE"}))
@ -219,8 +211,7 @@ public class OtaPackageController extends BaseController {
notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " +
"Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH) "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/otaPackage/{otaPackageId}")
@ResponseBody
public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION) public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(OTA_PACKAGE_ID, strOtaPackageId);

133
application/src/main/java/org/thingsboard/server/controller/TelemetryController.java

@ -33,11 +33,11 @@ import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -65,25 +65,17 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.IntervalType; import org.thingsboard.server.common.data.kv.IntervalType;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.exception.InvalidParametersException;
import org.thingsboard.server.exception.UncheckedApiException;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.SecurityUser;
@ -156,9 +148,6 @@ public class TelemetryController extends BaseController {
@Autowired @Autowired
private TbTelemetryService tbTelemetryService; private TbTelemetryService tbTelemetryService;
@Value("${transport.json.max_string_value_length:0}")
private int maxStringValueLength;
private ExecutorService executor; private ExecutorService executor;
@PostConstruct @PostConstruct
@ -314,10 +303,10 @@ public class TelemetryController extends BaseController {
@Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.") @Parameter(description = "A string value representing the timezone that will be used to calculate exact timestamps for 'WEEK', 'WEEK_ISO', 'MONTH' and 'QUARTER' interval types.")
@RequestParam(name = "timeZone", required = false) String timeZone, @RequestParam(name = "timeZone", required = false) String timeZone,
@Parameter(description = "An integer value that represents a max number of time series data points to fetch." + @Parameter(description = "An integer value that represents a max number of time series data points to fetch." +
" This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100")) " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", schema = @Schema(defaultValue = "100"))
@RequestParam(name = "limit", defaultValue = "100") Integer limit, @RequestParam(name = "limit", defaultValue = "100") Integer limit,
@Parameter(description = "A string value representing the aggregation function. " + @Parameter(description = "A string value representing the aggregation function. " +
"If the interval is not specified, 'agg' parameter will use 'NONE' value.", "If the interval is not specified, 'agg' parameter will use 'NONE' value.",
schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"})) schema = @Schema(allowableValues = {"MIN", "MAX", "AVG", "SUM", "COUNT", "NONE"}))
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr, @RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@ -337,20 +326,21 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK + @ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."),
@ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), @ApiResponse(responseCode = "400", description = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST),
@ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{deviceId}/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveDeviceAttributes(@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true)
public DeferredResult<ResponseEntity> saveDeviceAttributes( @PathVariable("deviceId") String deviceIdStr,
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -367,13 +357,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveEntityAttributesV1(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
public DeferredResult<ResponseEntity> saveEntityAttributesV1( @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -390,13 +382,15 @@ public class TelemetryController extends BaseController {
@ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), @ApiResponse(responseCode = "500", description = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) @PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}")
@ResponseBody public DeferredResult<ResponseEntity> saveEntityAttributesV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
public DeferredResult<ResponseEntity> saveEntityAttributesV2( @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope, @Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request); return saveAttributes(getTenantId(), entityId, scope, request);
} }
@ -460,11 +454,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " + @ApiResponse(responseCode = "200", description = "Time series for the selected keys in the request was removed. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity time series for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity time series removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ -541,11 +535,11 @@ public class TelemetryController extends BaseController {
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Device attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ -563,11 +557,11 @@ public class TelemetryController extends BaseController {
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " + @ApiResponse(responseCode = "200", description = "Entity attributes was removed for the selected keys in the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."),
@ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."), @ApiResponse(responseCode = "400", description = "Platform returns a bad request in case if keys or scope are not specified."),
@ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), @ApiResponse(responseCode = "401", description = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."),
@ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " + @ApiResponse(responseCode = "500", description = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."),
}) })
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
@ -616,18 +610,24 @@ public class TelemetryController extends BaseController {
}); });
} }
private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, JsonNode json) throws ThingsboardException { private DeferredResult<ResponseEntity> saveAttributes(TenantId srcTenantId, EntityId entityIdSrc, AttributeScope scope, String jsonStr) throws ThingsboardException {
if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) { if (AttributeScope.SERVER_SCOPE != scope && AttributeScope.SHARED_SCOPE != scope) {
return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
} }
if (json.isObject()) { JsonElement json;
List<AttributeKvEntry> attributes = extractRequestAttributes(json); try {
json = JsonParser.parseString(jsonStr);
} catch (Exception e) {
return getImmediateDeferredResult("Invalid JSON", HttpStatus.BAD_REQUEST);
}
if (json.isJsonObject()) {
List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json);
if (attributes.isEmpty()) { if (attributes.isEmpty()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
} }
for (AttributeKvEntry attributeKvEntry : attributes) { for (AttributeKvEntry attributeKvEntry : attributes) {
if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) { if (attributeKvEntry.getKey().isBlank()) {
return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST); return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST);
} }
} }
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -885,43 +885,6 @@ public class TelemetryController extends BaseController {
return result; return result;
} }
private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
long ts = System.currentTimeMillis();
List<AttributeKvEntry> attributes = new ArrayList<>();
jsonNode.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (entry.getValue().isObject() || entry.getValue().isArray()) {
attributes.add(new BaseAttributeKvEntry(new JsonDataEntry(key, toJsonStr(value)), ts));
} else if (entry.getValue().isTextual()) {
if (maxStringValueLength > 0 && entry.getValue().textValue().length() > maxStringValueLength) {
String message = String.format("String value length [%d] for key [%s] is greater than maximum allowed [%d]", entry.getValue().textValue().length(), key, maxStringValueLength);
throw new UncheckedApiException(new InvalidParametersException(message));
}
attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
} else if (entry.getValue().isBoolean()) {
attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
} else if (entry.getValue().isDouble()) {
attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
} else if (entry.getValue().isNumber()) {
if (entry.getValue().isBigInteger()) {
throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
} else {
attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
}
}
});
return attributes;
}
private String toJsonStr(JsonNode value) {
try {
return JacksonUtil.toString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException("Can't parse jsonValue: " + value, e);
}
}
private JsonNode toJsonNode(String value) { private JsonNode toJsonNode(String value) {
try { try {
return JacksonUtil.toJsonNode(value); return JacksonUtil.toJsonNode(value);

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

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

4
application/src/main/java/org/thingsboard/server/service/ai/AiModelService.java → application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java

@ -15,6 +15,6 @@
*/ */
package org.thingsboard.server.service.ai; package org.thingsboard.server.service.ai;
import org.thingsboard.rule.engine.api.RuleEngineAiModelService; import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
public interface AiModelService extends RuleEngineAiModelService {} public interface AiChatModelService extends RuleEngineAiChatModelService {}

9
application/src/main/java/org/thingsboard/server/service/ai/AiModelServiceImpl.java → application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

@ -21,21 +21,20 @@ import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.ChatResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
class AiModelServiceImpl implements AiModelService { class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer; private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor; private final AiRequestsExecutor aiRequestsExecutor;
@Override @Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest) { public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer); ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest); return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
} }
} }

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

@ -24,20 +24,26 @@ import com.google.cloud.vertexai.api.GenerationConfig;
import com.google.cloud.vertexai.api.PredictionServiceClient; import com.google.cloud.vertexai.api.PredictionServiceClient;
import com.google.cloud.vertexai.api.PredictionServiceSettings; import com.google.cloud.vertexai.api.PredictionServiceSettings;
import com.google.cloud.vertexai.generativeai.GenerativeModel; import com.google.cloud.vertexai.generativeai.GenerativeModel;
import dev.langchain4j.model.anthropic.AnthropicChatModel;
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
import dev.langchain4j.model.bedrock.BedrockChatModel; import dev.langchain4j.model.bedrock.BedrockChatModel;
import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel; import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel; 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.MistralAiChatModel; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel; 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;
@ -54,61 +60,57 @@ import java.time.Duration;
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override @Override
public ChatModel configureChatModel(OpenAiChatModel chatModel) { public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
OpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return OpenAiChatModel.builder()
return dev.langchain4j.model.openai.OpenAiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) { public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModel.providerConfig(); AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig(); return AzureOpenAiChatModel.builder()
return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint()) .endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion()) .serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey()) .apiKey(providerConfig.apiKey())
.deploymentName(modelConfig.modelId()) .deploymentName(chatModelConfig.modelId())
.temperature(modelConfig.temperature()) .temperature(chatModelConfig.temperature())
.topP(modelConfig.topP()) .topP(chatModelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens()) .maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds())) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries()) .maxRetries(chatModelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) { public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig(); return GoogleAiGeminiChatModel.builder()
return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .topK(chatModelConfig.topK())
.topK(modelConfig.topK()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxOutputTokens(chatModelConfig.maxOutputTokens())
.maxOutputTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) { public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig(); GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
// construct service account credentials using service account key JSON // construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials; ServiceAccountCredentials serviceAccountCredentials;
@ -131,8 +133,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.toBuilder(); .toBuilder();
// set request timeout from model config // set request timeout from model config
if (modelConfig.timeoutSeconds() != null) { if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds())); retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
} }
// set updated retry settings // set updated retry settings
@ -154,30 +156,30 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// map model config to generation config // map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder(); var generationConfigBuilder = GenerationConfig.newBuilder();
if (modelConfig.temperature() != null) { if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue()); generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
} }
if (modelConfig.topP() != null) { if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(modelConfig.topP().floatValue()); generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
} }
if (modelConfig.topK() != null) { if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(modelConfig.topK()); generationConfigBuilder.setTopK(chatModelConfig.topK());
} }
if (modelConfig.frequencyPenalty() != null) { if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue()); generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
} }
if (modelConfig.frequencyPenalty() != null) { if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue()); generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
} }
if (modelConfig.maxOutputTokens() != null) { if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens()); generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
} }
var generationConfig = generationConfigBuilder.build(); var generationConfig = generationConfigBuilder.build();
// construct generative model instance // construct generative model instance
var generativeModel = new GenerativeModel(modelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig); var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig);
return new VertexAiGeminiChatModel(generativeModel, generationConfig, modelConfig.maxRetries()); return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries());
} }
private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) { private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) {
@ -189,40 +191,37 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
} }
@Override @Override
public ChatModel configureChatModel(MistralAiChatModel chatModel) { public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) {
MistralAiChatModel.Config modelConfig = chatModel.modelConfig(); return MistralAiChatModel.builder()
return dev.langchain4j.model.mistralai.MistralAiChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AnthropicChatModel chatModel) { public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) {
AnthropicChatModel.Config modelConfig = chatModel.modelConfig(); return AnthropicChatModel.builder()
return dev.langchain4j.model.anthropic.AnthropicChatModel.builder() .apiKey(chatModelConfig.providerConfig().apiKey())
.apiKey(chatModel.providerConfig().apiKey()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .topK(chatModelConfig.topK())
.topK(modelConfig.topK()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) { public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig(); AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig();
var credentialsProvider = StaticCredentialsProvider.create( var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey()) AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
@ -234,33 +233,32 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build(); .build();
var defaultChatRequestParams = ChatRequestParameters.builder() var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(modelConfig.temperature()) .temperature(chatModelConfig.temperature())
.topP(modelConfig.topP()) .topP(chatModelConfig.topP())
.maxOutputTokens(modelConfig.maxOutputTokens()) .maxOutputTokens(chatModelConfig.maxOutputTokens())
.build(); .build();
return BedrockChatModel.builder() return BedrockChatModel.builder()
.client(bedrockClient) .client(bedrockClient)
.modelId(modelConfig.modelId()) .modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams) .defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(modelConfig.timeoutSeconds())) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries()) .maxRetries(chatModelConfig.maxRetries())
.build(); .build();
} }
@Override @Override
public ChatModel configureChatModel(GitHubModelsChatModel chatModel) { public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
GitHubModelsChatModel.Config modelConfig = chatModel.modelConfig(); return GitHubModelsChatModel.builder()
return dev.langchain4j.model.github.GitHubModelsChatModel.builder() .gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
.gitHubToken(chatModel.providerConfig().personalAccessToken()) .modelName(chatModelConfig.modelId())
.modelName(modelConfig.modelId()) .temperature(chatModelConfig.temperature())
.temperature(modelConfig.temperature()) .topP(chatModelConfig.topP())
.topP(modelConfig.topP()) .frequencyPenalty(chatModelConfig.frequencyPenalty())
.frequencyPenalty(modelConfig.frequencyPenalty()) .presencePenalty(chatModelConfig.presencePenalty())
.presencePenalty(modelConfig.presencePenalty()) .maxTokens(chatModelConfig.maxOutputTokens())
.maxTokens(modelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds()))
.timeout(toDuration(modelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries())
.maxRetries(modelConfig.maxRetries())
.build(); .build();
} }

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

@ -29,6 +29,7 @@ import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -61,6 +62,7 @@ import org.thingsboard.server.service.edge.rpc.processor.alarm.AlarmProcessor;
import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor; import org.thingsboard.server.service.edge.rpc.processor.alarm.comment.AlarmCommentProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.AssetEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.asset.profile.AssetProfileEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.cf.CalculatedFieldProcessor;
import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.dashboard.DashboardEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.DeviceEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor;
@ -248,6 +250,12 @@ public class EdgeContextComponent {
@Autowired @Autowired
private GrpcCallbackExecutorService grpcCallbackExecutorService; private GrpcCallbackExecutorService grpcCallbackExecutorService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private CalculatedFieldProcessor calculatedFieldProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType); EdgeProcessor processor = processorMap.get(edgeEventType);
if (processor == null) { if (processor == null) {

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

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.alarm.EntityAlarm;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.Domain; import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -112,7 +113,7 @@ public class EdgeEventSourcingListener {
return; return;
} }
try { try {
if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL_SETTINGS == entityType) { if (EntityType.TENANT == entityType || EntityType.EDGE == entityType || EntityType.AI_MODEL == entityType) {
return; return;
} }
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event); log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -226,7 +227,7 @@ public class EdgeEventSourcingListener {
break; break;
case TENANT: case TENANT:
return !event.getCreated(); return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS: case API_USAGE_STATE, EDGE, AI_MODEL:
return false; return false;
case DOMAIN: case DOMAIN:
if (entity instanceof Domain domain) { if (entity instanceof Domain domain) {
@ -262,6 +263,8 @@ public class EdgeEventSourcingListener {
private String getBodyMsgForEntityEvent(Object entity) { private String getBodyMsgForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) { if (entity instanceof AlarmComment) {
return JacksonUtil.toString(entity); return JacksonUtil.toString(entity);
} else if (entity instanceof CalculatedField calculatedField) {
return JacksonUtil.toString(calculatedField.getEntityId());
} }
return null; return null;
} }

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

@ -48,11 +48,13 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.domain.DomainInfo;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils {
.build(); .build();
} }
public static CalculatedFieldUpdateMsg constructCalculatedFieldUpdatedMsg(UpdateMsgType msgType, CalculatedField calculatedField) {
return CalculatedFieldUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(calculatedField))
.setIdMSB(calculatedField.getId().getId().getMostSignificantBits())
.setIdLSB(calculatedField.getId().getId().getLeastSignificantBits()).build();
}
public static CalculatedFieldUpdateMsg constructCalculatedFieldDeleteMsg(CalculatedFieldId calculatedFieldId) {
return CalculatedFieldUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(calculatedFieldId.getId().getMostSignificantBits())
.setIdLSB(calculatedFieldId.getId().getLeastSignificantBits()).build();
}
} }

2
application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java

@ -68,7 +68,7 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener( @TransactionalEventListener(
fallbackExecution = true, fallbackExecution = true,
condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL_SETTINGS" condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL"
) )
public void handleEvent(DeleteEntityEvent<?> event) { public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {

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

@ -94,6 +94,8 @@ 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<>();
@ -283,9 +285,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
EdgeGrpcSession session = sessions.get(edgeId); EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) { if (session != null && session.isConnected()) {
log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId); log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
session.destroy(); destroySession(session);
session.cleanUp(); session.cleanUp();
session.close();
sessions.remove(edgeId); sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock(); newEventLock.lock();
@ -521,7 +522,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession session) { private void destroySession(EdgeGrpcSession session) {
try (session) { try (session) {
session.destroy(); for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) {
if (session.destroy()) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
}
}
} }
} }
@ -643,9 +652,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
for (EdgeId edgeId : toRemove) { for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId); log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId); EdgeGrpcSession removed = sessions.get(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) { if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
kafkaSession.destroy(); if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
} }
} }
} catch (Exception e) { } catch (Exception e) {

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

@ -50,6 +50,8 @@ import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg; import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectResponseCode; import org.thingsboard.server.gen.edge.v1.ConnectResponseCode;
import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg; import org.thingsboard.server.gen.edge.v1.ConnectResponseMsg;
@ -452,14 +454,15 @@ public abstract class EdgeGrpcSession implements Closeable {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) { if (attempt > 1) {
String error = "Failed to deliver the batch"; String error = "Failed to deliver the batch";
String failureMsg = String.format("{%s}: {%s}", error, copy); String failureMsg = String.format("{%s} (size: {%s})", error, copy.size());
if (attempt == 2) { if (attempt == 2) {
// Send a failure notification only on the second attempt. // Send a failure notification only on the second attempt.
// This ensures that failure alerts are sent just once to avoid redundant notifications. // This ensures that failure alerts are sent just once to avoid redundant notifications.
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
} }
log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt); log.warn("[{}][{}] {} on attempt {}", tenantId, edge.getId(), failureMsg, attempt);
log.debug("[{}][{}] entities in failed batch: {}", tenantId, edge.getId(), copy);
} }
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size()); log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size());
for (DownlinkMsg downlinkMsg : copy) { for (DownlinkMsg downlinkMsg : copy) {
@ -882,6 +885,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg)); result.add(ctx.getEdgeRequestsService().processRelationRequestMsg(edge.getTenantId(), edge, relationRequestMsg));
} }
} }
if (uplinkMsg.getCalculatedFieldRequestMsgCount() > 0) {
for (CalculatedFieldRequestMsg calculatedFieldRequestMsg : uplinkMsg.getCalculatedFieldRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processCalculatedFieldRequestMsg(edge.getTenantId(), edge, calculatedFieldRequestMsg));
}
}
if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) { if (uplinkMsg.getUserCredentialsRequestMsgCount() > 0) {
for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) { for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg)); result.add(ctx.getEdgeRequestsService().processUserCredentialsRequestMsg(edge.getTenantId(), edge, userCredentialsRequestMsg));
@ -907,6 +915,11 @@ public abstract class EdgeGrpcSession implements Closeable {
result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg)); result.add(ctx.getEdgeRequestsService().processEntityViewsRequestMsg(edge.getTenantId(), edge, entityViewRequestMsg));
} }
} }
if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
}
}
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
@ -917,7 +930,9 @@ public abstract class EdgeGrpcSession implements Closeable {
return Futures.allAsList(result); return Futures.allAsList(result);
} }
protected void destroy() {} protected boolean destroy() {
return true;
}
protected void cleanUp() {} protected void cleanUp() {}

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

@ -107,4 +107,5 @@ public class EdgeSyncCursor {
currentIdx++; currentIdx++;
return edgeEventFetcher; return edgeEventFetcher;
} }
} }

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

@ -135,19 +135,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
} }
@Override @Override
public void destroy() { public boolean destroy() {
try { try {
if (consumer != null) { if (consumer != null) {
consumer.stop(); consumer.stop();
} }
} finally { } catch (Exception e) {
consumer = null; log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e);
return false;
} }
consumer = null;
try { try {
if (consumerExecutor != null) { if (consumerExecutor != null) {
consumerExecutor.shutdown(); consumerExecutor.shutdown();
} }
} catch (Exception ignored) {} } catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
} }
@Override @Override

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

@ -139,8 +139,8 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
UPDATED_COMMENT, DELETED -> true; UPDATED_COMMENT, DELETED -> true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE,
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
NOTIFICATION_RULE -> true; NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false; default -> false;
}; };
}; };
@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
if (edgeId != null && !edgeId.equals(originatorEdgeId)) { if (edgeId != null && !edgeId.equals(originatorEdgeId)) {
return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body); return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
} else { } else {
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId); return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId);
} }
case DELETED: case DELETED:
EdgeEventActionType deleted = EdgeEventActionType.DELETED; EdgeEventActionType deleted = EdgeEventActionType.DELETED;
@ -260,11 +260,11 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
} }
} }
private ListenableFuture<Void> processNotificationToRelatedEdges(TenantId tenantId, EntityId entityId, EdgeEventType type, protected ListenableFuture<Void> processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type,
EdgeEventActionType actionType, EdgeId sourceEdgeId) { EdgeEventActionType actionType, EdgeId sourceEdgeId) {
List<ListenableFuture<Void>> futures = new ArrayList<>(); List<ListenableFuture<Void>> futures = new ArrayList<>();
PageDataIterableByTenantIdEntityId<EdgeId> edgeIds = PageDataIterableByTenantIdEntityId<EdgeId> edgeIds =
new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, entityId, RELATED_EDGES_CACHE_ITEMS); new PageDataIterableByTenantIdEntityId<>(edgeCtx.getEdgeService()::findRelatedEdgeIdsByEntityId, tenantId, ownerEntityId, RELATED_EDGES_CACHE_ITEMS);
for (EdgeId relatedEdgeId : edgeIds) { for (EdgeId relatedEdgeId : edgeIds) {
if (!relatedEdgeId.equals(sourceEdgeId)) { if (!relatedEdgeId.equals(sourceEdgeId)) {
futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null)); futures.add(saveEdgeEvent(tenantId, relatedEdgeId, type, actionType, entityId, null));

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

@ -119,6 +119,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAssetUpdateMsg(assetUpdateMsg); .addAssetUpdateMsg(assetUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId()); AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile)); builder.addAssetProfileUpdateMsg(EdgeMsgConstructorUtils.constructAssetProfileUpdatedMsg(msgType, assetProfile));

79
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/BaseCalculatedFieldProcessor.java

@ -0,0 +1,79 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor;
@Slf4j
public abstract class BaseCalculatedFieldProcessor extends BaseEdgeProcessor {
@Autowired
private DataValidator<CalculatedField> calculatedFieldValidator;
protected Pair<Boolean, Boolean> saveOrUpdateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) {
boolean isCreated = false;
boolean isNameUpdated = false;
try {
CalculatedField calculatedField = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
if (calculatedField == null) {
throw new RuntimeException("[{" + tenantId + "}] calculatedFieldUpdateMsg {" + calculatedFieldUpdateMsg + " } cannot be converted to calculatedField");
}
CalculatedField calculatedFieldById = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
if (calculatedFieldById == null) {
calculatedField.setCreatedTime(Uuids.unixTimestamp(calculatedFieldId.getId()));
isCreated = true;
calculatedField.setId(null);
} else {
calculatedField.setId(calculatedFieldId);
}
String calculatedFieldName = calculatedField.getName();
CalculatedField calculatedFieldByName = edgeCtx.getCalculatedFieldService().findByEntityIdAndName(calculatedField.getEntityId(), calculatedFieldName);
if (calculatedFieldByName != null && !calculatedFieldByName.getId().equals(calculatedFieldId)) {
calculatedFieldName = calculatedFieldName + "_" + StringUtils.randomAlphabetic(15);
log.warn("[{}] calculatedField with name {} already exists. Renaming calculatedField name to {}",
tenantId, calculatedField.getName(), calculatedFieldByName.getName());
isNameUpdated = true;
}
calculatedField.setName(calculatedFieldName);
calculatedFieldValidator.validate(calculatedField, CalculatedField::getTenantId);
if (isCreated) {
calculatedField.setId(calculatedFieldId);
}
edgeCtx.getCalculatedFieldService().save(calculatedField, false);
} catch (Exception e) {
log.error("[{}] Failed to process calculatedField update msg [{}]", tenantId, calculatedFieldUpdateMsg, e);
throw e;
}
return Pair.of(isCreated, isNameUpdated);
}
}

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

@ -0,0 +1,169 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.UUID;
@Slf4j
@Component
@TbCoreComponent
public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor implements CalculatedFieldProcessor {
@Override
public ListenableFuture<Void> processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg) {
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID(calculatedFieldUpdateMsg.getIdMSB(), calculatedFieldUpdateMsg.getIdLSB()));
try {
edgeSynchronizationManager.getEdgeId().set(edge.getId());
switch (calculatedFieldUpdateMsg.getMsgType()) {
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
processCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg, edge);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
if (calculatedField != null) {
edgeCtx.getCalculatedFieldService().deleteCalculatedField(tenantId, calculatedFieldId);
}
return Futures.immediateFuture(null);
case UNRECOGNIZED:
default:
return handleUnsupportedMsgType(calculatedFieldUpdateMsg.getMsgType());
}
} catch (DataValidationException e) {
if (e.getMessage().contains("limit reached")) {
log.warn("[{}] Number of allowed calculatedField violated {}", tenantId, calculatedFieldUpdateMsg, e);
return Futures.immediateFuture(null);
} else {
return Futures.immediateFailedFuture(e);
}
} finally {
edgeSynchronizationManager.getEdgeId().remove();
}
}
@Override
public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) {
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(edgeEvent.getEntityId());
switch (edgeEvent.getAction()) {
case ADDED, UPDATED -> {
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(edgeEvent.getTenantId(), calculatedFieldId);
if (calculatedField != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldUpdatedMsg(msgType, calculatedField);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg)
.build();
}
}
case DELETED -> {
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = EdgeMsgConstructorUtils.constructCalculatedFieldDeleteMsg(calculatedFieldId);
return DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsg)
.build();
}
}
return null;
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.CALCULATED_FIELD;
}
@Override
public ListenableFuture<Void> processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg) {
EdgeEventType type = EdgeEventType.valueOf(edgeNotificationMsg.getType());
EdgeEventActionType actionType = EdgeEventActionType.valueOf(edgeNotificationMsg.getAction());
EntityId entityId = EntityIdFactory.getByEdgeEventTypeAndUuid(type, new UUID(edgeNotificationMsg.getEntityIdMSB(), edgeNotificationMsg.getEntityIdLSB()));
EdgeId originatorEdgeId = safeGetEdgeId(edgeNotificationMsg.getOriginatorEdgeIdMSB(), edgeNotificationMsg.getOriginatorEdgeIdLSB());
switch (actionType) {
case UPDATED:
case ADDED:
EntityId calculatedFieldOwnerId = JacksonUtil.fromString(edgeNotificationMsg.getBody(), EntityId.class);
if (calculatedFieldOwnerId != null &&
(EntityType.DEVICE.equals(calculatedFieldOwnerId.getEntityType()) || EntityType.ASSET.equals(calculatedFieldOwnerId.getEntityType()))) {
JsonNode body = JacksonUtil.toJsonNode(edgeNotificationMsg.getBody());
EdgeId edgeId = safeGetEdgeId(edgeNotificationMsg.getEdgeIdMSB(), edgeNotificationMsg.getEdgeIdLSB());
return edgeId != null ?
saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body) :
processNotificationToRelatedEdges(tenantId, calculatedFieldOwnerId, entityId, type, actionType, originatorEdgeId);
} else {
return processActionForAllEdges(tenantId, type, actionType, entityId, null, originatorEdgeId);
}
default:
return super.processEntityNotification(tenantId, edgeNotificationMsg);
}
}
private void processCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg, Edge edge) {
Pair<Boolean, Boolean> resultPair = super.saveOrUpdateCalculatedField(tenantId, calculatedFieldId, calculatedFieldUpdateMsg);
Boolean wasCreated = resultPair.getFirst();
if (wasCreated) {
pushCalculatedFieldCreatedEventToRuleEngine(tenantId, edge, calculatedFieldId);
}
Boolean nameWasUpdated = resultPair.getSecond();
if (nameWasUpdated) {
saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.CALCULATED_FIELD, EdgeEventActionType.UPDATED, calculatedFieldId, null);
}
}
private void pushCalculatedFieldCreatedEventToRuleEngine(TenantId tenantId, Edge edge, CalculatedFieldId calculatedFieldId) {
try {
CalculatedField calculatedField = edgeCtx.getCalculatedFieldService().findById(tenantId, calculatedFieldId);
String calculatedFieldAsString = JacksonUtil.toString(calculatedField);
TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, edge.getCustomerId());
pushEntityEventToRuleEngine(tenantId, calculatedFieldId, edge.getCustomerId(), TbMsgType.ENTITY_CREATED, calculatedFieldAsString, msgMetaData);
} catch (Exception e) {
log.warn("[{}][{}] Failed to push calculatedField action to rule engine: {}", tenantId, calculatedFieldId, TbMsgType.ENTITY_CREATED.name(), e);
}
}
}

28
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldProcessor.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.cf;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor;
public interface CalculatedFieldProcessor extends EdgeProcessor {
ListenableFuture<Void> processCalculatedFieldMsgFromEdge(TenantId tenantId, Edge edge, CalculatedFieldUpdateMsg calculatedFieldUpdateMsg);
}

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

@ -243,6 +243,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr
DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials); DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructDeviceCredentialsUpdatedMsg(deviceCredentials);
builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build();
} }
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile)); builder.addDeviceProfileUpdateMsg(EdgeMsgConstructorUtils.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java

@ -266,7 +266,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create(); SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE));
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json, ts);
ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes); ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes);
Futures.addCallback(future, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@Override @Override
@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create(); SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE)); AttributeScope scope = AttributeScope.valueOf(metaData.getValue(DataConstants.SCOPE));
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(json, ts)); List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json, ts);
ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes); ListenableFuture<List<AttributeKvEntry>> future = filterAttributesByTs(tenantId, entityId, scope, attributes);
Futures.addCallback(future, new FutureCallback<>() { Futures.addCallback(future, new FutureCallback<>() {
@Override @Override

45
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -54,12 +54,14 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
@ -90,7 +92,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired @Autowired
private TimeseriesService timeseriesService; private TimeseriesService timeseriesService;
@Autowired @Autowired
private RelationService relationService; private RelationService relationService;
@ -104,6 +106,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired @Autowired
private WidgetTypeService widgetTypeService; private WidgetTypeService widgetTypeService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired @Autowired
private DbCallbackExecutorService dbCallbackExecutorService; private DbCallbackExecutorService dbCallbackExecutorService;
@ -293,6 +298,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
return futureToSet; return futureToSet;
} }
@Override
public ListenableFuture<Void> processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg) {
log.trace("[{}] processCalculatedFieldRequestMsg [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(
EntityType.valueOf(calculatedFieldRequestMsg.getEntityType()),
new UUID(calculatedFieldRequestMsg.getEntityIdMSB(), calculatedFieldRequestMsg.getEntityIdLSB()));
log.trace("[{}] processCalculatedField [{}][{}] for entity [{}][{}]", tenantId, edge.getName(), calculatedFieldRequestMsg, entityId.getEntityType(), entityId.getId());
return saveCalculatedFieldsToEdge(tenantId, edge.getId(), entityId);
}
private ListenableFuture<Void> saveCalculatedFieldsToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId) {
return Futures.transformAsync(
dbCallbackExecutorService.submit(() -> calculatedFieldService.findCalculatedFieldsByEntityId(tenantId, entityId)),
calculatedFields -> {
log.trace("[{}][{}][{}][{}] calculatedField(s) are going to be pushed to edge.", tenantId, edgeId, entityId, calculatedFields.size());
List<ListenableFuture<?>> futures = calculatedFields.stream().map(calculatedField -> {
try {
return saveEdgeEvent(tenantId, edgeId, EdgeEventType.CALCULATED_FIELD,
EdgeEventActionType.ADDED, calculatedField.getId(), JacksonUtil.valueToTree(calculatedField));
} catch (Exception e) {
log.error("[{}][{}] Exception during loading calculatedField [{}] to edge on sync!", tenantId, edgeId, calculatedField, e);
return Futures.immediateFailedFuture(e);
}
}).toList();
return Futures.transform(
Futures.allAsList(futures),
voids -> null,
dbCallbackExecutorService
);
},
dbCallbackExecutorService
);
}
private ListenableFuture<List<EntityRelation>> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) { private ListenableFuture<List<EntityRelation>> findRelationByQuery(TenantId tenantId, Edge edge, EntityId entityId, EntitySearchDirection direction) {
EntityRelationsQuery query = new EntityRelationsQuery(); EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false)); query.setParameters(new RelationsSearchParameters(entityId, direction, 1, false));

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg; import org.thingsboard.server.gen.edge.v1.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg; import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
@ -35,6 +36,8 @@ public interface EdgeRequestsService {
ListenableFuture<Void> processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); ListenableFuture<Void> processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg);
ListenableFuture<Void> processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true) @Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg);
@ -46,4 +49,5 @@ public interface EdgeRequestsService {
@Deprecated(since = "3.9.1", forRemoval = true) @Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg);
} }

36
application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelSettingsService.java → application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java

@ -19,9 +19,9 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -30,48 +30,48 @@ import static java.util.Objects.requireNonNullElseGet;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService { class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelSettingsService aiModelSettingsService; private final AiModelService aiModelService;
@Override @Override
public AiModelSettings save(AiModelSettings settings, User user) { public AiModel save(AiModel model, User user) {
var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED; var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId(); var tenantId = user.getTenantId();
settings.setTenantId(tenantId); model.setTenantId(tenantId);
AiModelSettings savedSettings; AiModel savedModel;
try { try {
savedSettings = aiModelSettingsService.save(settings); savedModel = aiModelService.save(model);
autoCommit(user, savedSettings.getId()); autoCommit(user, savedModel.getId());
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(settings.getId(), () -> emptyId(EntityType.AI_MODEL_SETTINGS)), settings, actionType, user, e); logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e);
throw e; throw e;
} }
logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user); logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedSettings; return savedModel;
} }
@Override @Override
public boolean delete(AiModelSettings settings, User user) { public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED; var actionType = ActionType.DELETED;
var tenantId = user.getTenantId(); var tenantId = user.getTenantId();
var settingsId = settings.getId(); var modelId = model.getId();
boolean deleted; boolean deleted;
try { try {
deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId); deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} catch (Exception e) { } catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, e, settingsId.toString()); logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString());
throw e; throw e;
} }
if (deleted) { if (deleted) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString()); logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
} }
return deleted; return deleted;

8
application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelSettingsService.java → application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java

@ -16,12 +16,12 @@
package org.thingsboard.server.service.entitiy.ai; package org.thingsboard.server.service.entitiy.ai;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
public interface TbAiModelSettingsService { public interface TbAiModelService {
AiModelSettings save(AiModelSettings settings, User user); AiModel save(AiModel model, User user);
boolean delete(AiModelSettings settings, User user); boolean delete(AiModel model, User user);
} }

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

@ -110,4 +110,5 @@ public class DefaultTbOtaPackageService extends AbstractTbEntityService implemen
throw e; throw e;
} }
} }
} }

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

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade. // This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.0.0", "4.0.1"); private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0");
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;

101
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -20,25 +20,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
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;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService; import org.thingsboard.server.service.install.DbUpgradeExecutorService;
@ -46,108 +38,27 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE;
@Service @Service
@Profile("install") @Profile("install")
@Slf4j @Slf4j
@RequiredArgsConstructor
public class DefaultDataUpdateService implements DataUpdateService { public class DefaultDataUpdateService implements DataUpdateService {
private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256; private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256;
private static final int DEFAULT_PAGE_SIZE = 1024; private static final int DEFAULT_PAGE_SIZE = 1024;
@Autowired private final RuleChainService ruleChainService;
private RuleChainService ruleChainService; private final ComponentDiscoveryService componentDiscoveryService;
private final DbUpgradeExecutorService executorService;
@Autowired
private RelationService relationService;
@Autowired
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private DbUpgradeExecutorService executorService;
@Autowired
private TenantProfileService tenantProfileService;
@Override @Override
public void updateData() throws Exception { public void updateData() throws Exception {
log.info("Updating data ..."); log.info("Updating data ...");
//TODO: should be cleaned after each release //TODO: should be cleaned after each release
updateInputNodes();
deduplicateRateLimitsPerSecondsConfigurations();
log.info("Data updated.");
}
private void deduplicateRateLimitsPerSecondsConfigurations() {
log.info("Starting update of tenant profiles...");
int totalProfiles = 0; log.info("Data updated.");
int updatedTenantProfiles = 0;
int skippedProfiles = 0;
int failedProfiles = 0;
var tenantProfiles = new PageDataIterable<>(
pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024);
for (TenantProfile tenantProfile : tenantProfiles) {
totalProfiles++;
String profileName = tenantProfile.getName();
UUID profileId = tenantProfile.getId().getId();
try {
Optional<DefaultTenantProfileConfiguration> profileConfiguration = tenantProfile.getProfileConfiguration();
if (profileConfiguration.isEmpty()) {
log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName);
skippedProfiles++;
continue;
}
DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get();
defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs();
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
updatedTenantProfiles++;
log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName);
} catch (Exception e) {
log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e);
failedProfiles++;
}
}
log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}",
totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles);
}
private void updateInputNodes() {
log.info("Creating relations for input nodes...");
int n = 0;
var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024);
for (RuleNode inputNode : inputNodes) {
try {
RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId"))
.filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id)))
.orElse(null);
if (targetRuleChainId == null) {
continue;
}
EntityRelation relation = new EntityRelation();
relation.setFrom(inputNode.getRuleChainId());
relation.setTo(targetRuleChainId);
relation.setType(EntityRelation.USES_TYPE);
relation.setTypeGroup(RelationTypeGroup.COMMON);
relationService.saveRelation(TenantId.SYS_TENANT_ID, relation);
n++;
} catch (Exception e) {
log.error("Failed to save relation for input node: {}", inputNode, e);
}
}
log.info("Created {} relations for input nodes", n);
} }
@Override @Override

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

@ -53,7 +53,7 @@ public enum Resource {
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS, MOBILE_APP_SETTINGS,
JOB(EntityType.JOB), JOB(EntityType.JOB),
AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS); AI_MODEL(EntityType.AI_MODEL);
private final Set<EntityType> entityTypes; private final Set<EntityType> entityTypes;

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

@ -18,8 +18,8 @@ package org.thingsboard.server.service.security.permission;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -58,7 +58,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP, tenantEntityPermissionChecker);
put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker); put(Resource.JOB, tenantEntityPermissionChecker);
put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker); put(Resource.AI_MODEL, aiModelPermissionChecker);
} }
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
@ -149,7 +149,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
}; };
private static final PermissionChecker<AiModelSettingsId, AiModelSettings> aiModelSettingsPermissionChecker = new PermissionChecker<>() { private static final PermissionChecker<AiModelId, AiModel> aiModelPermissionChecker = new PermissionChecker<>() {
@Override @Override
public boolean hasPermission(SecurityUser user, Operation operation) { public boolean hasPermission(SecurityUser user, Operation operation) {
@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
} }
@Override @Override
public boolean hasPermission(SecurityUser user, Operation operation, AiModelSettingsId entityId, AiModelSettings entity) { public boolean hasPermission(SecurityUser user, Operation operation, AiModelId entityId, AiModel entity) {
return user.getTenantId().equals(entity.getTenantId()); return user.getTenantId().equals(entity.getTenantId());
} }

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

@ -67,10 +67,10 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of( protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE, EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.DASHBOARD, EntityType.ASSET_PROFILE, EntityType.ASSET,
EntityType.DEVICE_PROFILE, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE, EntityType.ENTITY_VIEW, EntityType.WIDGET_TYPE, EntityType.WIDGETS_BUNDLE,
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.AI_MODEL_SETTINGS EntityType.AI_MODEL
); );
@Override @Override

8
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelSettingsExportService.java → application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java

@ -17,8 +17,8 @@ package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -26,11 +26,11 @@ import java.util.Set;
@Service @Service
@TbCoreComponent @TbCoreComponent
class AiModelSettingsExportService extends BaseEntityExportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> { class AiModelExportService extends BaseEntityExportService<AiModelId, AiModel, EntityExportData<AiModel>> {
@Override @Override
public Set<EntityType> getSupportedEntityTypes() { public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.AI_MODEL_SETTINGS); return Set.of(EntityType.AI_MODEL);
} }
} }

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java

@ -38,6 +38,8 @@ public class DeviceExportService extends BaseEntityExportService<DeviceId, Devic
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Device device, DeviceExportData exportData) { protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Device device, DeviceExportData exportData) {
device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId())); device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId())); device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId()));
device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId()));
if (ctx.getSettings().isExportCredentials()) { if (ctx.getSettings().isExportCredentials()) {
var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId()); var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
credentials.setId(null); credentials.setId(null);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java

@ -34,6 +34,8 @@ public class DeviceProfileExportService extends BaseEntityExportService<DevicePr
deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultDashboardId()));
deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setFirmwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getSoftwareId()));
} }
@Override @Override

49
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java

@ -0,0 +1,49 @@
/**
* 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.sync.ie.exporting.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageExportService extends BaseEntityExportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, OtaPackage otaPackage, OtaPackageExportData exportData) {
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE);
}
}

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

@ -67,7 +67,6 @@ import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils; import org.thingsboard.server.utils.CsvUtils;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -235,7 +234,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
@SneakyThrows @SneakyThrows
private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) { private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) {
String scope = kvType.getKey(); String scope = kvType.getKey();
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(kvsEntry.getValue())); List<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(kvsEntry.getValue());
accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> { accessValidator.validateEntityAndCallback(user, Operation.WRITE_ATTRIBUTES, entity.getId(), (result, tenantId, entityId) -> {
tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder() tsSubscriptionService.saveAttributes(AttributesSaveRequest.builder()

44
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelSettingsImportService.java → application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java

@ -18,60 +18,60 @@ package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.ai.AiModelSettingsService; import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service @Service
@TbCoreComponent @TbCoreComponent
@RequiredArgsConstructor @RequiredArgsConstructor
class AiModelSettingsImportService extends BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> { class AiModelImportService extends BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>> {
private final AiModelSettingsService aiModelSettingsService; private final AiModelService aiModelService;
@Override @Override
protected void setOwner( protected void setOwner(
TenantId tenantId, TenantId tenantId,
AiModelSettings settings, AiModel model,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) { ) {
settings.setTenantId(tenantId); model.setTenantId(tenantId);
} }
@Override @Override
protected AiModelSettings prepare( protected AiModel prepare(
EntitiesImportCtx ctx, EntitiesImportCtx ctx,
AiModelSettings settings, AiModel model,
AiModelSettings oldEntity, AiModel oldModel,
EntityExportData<AiModelSettings> exportData, EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) { ) {
return settings; return model;
} }
@Override @Override
protected AiModelSettings deepCopy(AiModelSettings settings) { protected AiModel deepCopy(AiModel model) {
return new AiModelSettings(settings); return new AiModel(model);
} }
@Override @Override
protected AiModelSettings saveOrUpdate( protected AiModel saveOrUpdate(
EntitiesImportCtx ctx, EntitiesImportCtx ctx,
AiModelSettings settings, AiModel model,
EntityExportData<AiModelSettings> exportData, EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider, BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider,
CompareResult compareResult CompareResult compareResult
) { ) {
return aiModelSettingsService.save(settings); return aiModelService.save(model);
} }
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS; return EntityType.AI_MODEL;
} }
} }

15
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -71,7 +71,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -148,6 +147,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
public CompareResult(boolean updateNeeded) { public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded; this.updateNeeded = updateNeeded;
} }
} }
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) { protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
@ -203,7 +203,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult); protected abstract E saveOrUpdate(EntitiesImportCtx ctx, E entity, D exportData, IdProvider idProvider, CompareResult compareResult);
protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException { protected void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
E savedEntity = importResult.getSavedEntity(); E savedEntity = importResult.getSavedEntity();
E oldEntity = importResult.getOldEntity(); E oldEntity = importResult.getOldEntity();
@ -405,7 +404,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
} }
public <ID extends EntityId> ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) { public <ID extends EntityId> ID getInternalId(ID externalId, boolean throwExceptionIfNotFound) {
if (externalId == null || externalId.isNullUid()) return null; if (externalId == null || externalId.isNullUid()) {
return null;
}
if (EntityType.TENANT.equals(externalId.getEntityType())) { if (EntityType.TENANT.equals(externalId.getEntityType())) {
return (ID) ctx.getTenantId(); return (ID) ctx.getTenantId();
@ -432,7 +433,9 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
} }
public Optional<EntityId> getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set<EntityType> hints) { public Optional<EntityId> getInternalIdByUuid(UUID externalUuid, boolean fetchAllUUIDs, Set<EntityType> hints) {
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); if (externalUuid.equals(EntityId.NULL_UUID)) {
return Optional.empty();
}
for (EntityType entityType : EntityType.values()) { for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid); Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
@ -483,10 +486,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
} }
protected <T extends EntityId, O> T getOldEntityField(O oldEntity, Function<O, T> getter) {
return oldEntity == null ? null : getter.apply(oldEntity);
}
protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json, protected void replaceIdsRecursively(EntitiesImportCtx ctx, IdProvider idProvider, JsonNode json,
Set<String> skippedRootFields, Pattern includedFieldsPattern, Set<String> skippedRootFields, Pattern includedFieldsPattern,
LinkedHashSet<EntityType> hints) { LinkedHashSet<EntityType> hints) {

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java

@ -44,8 +44,8 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override @Override
protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) { protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId())); device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId)); device.setFirmwareId(idProvider.getInternalId(device.getFirmwareId()));
device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId)); device.setSoftwareId(idProvider.getInternalId(device.getSoftwareId()));
return device; return device;
} }

13
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java

@ -45,15 +45,20 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId())); deviceProfile.setDefaultRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId())); deviceProfile.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId())); deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId)); deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId(), false));
deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId)); deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId(), false));
return deviceProfile; return deviceProfile;
} }
@Override @Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) { protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider, CompareResult compareResult) {
boolean toUpdate = ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds();
if (toUpdate) {
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId()));
}
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { if (toUpdate) {
importCalculatedFields(ctx, saved, exportData, idProvider); importCalculatedFields(ctx, saved, exportData, idProvider);
} }
return saved; return saved;
@ -73,8 +78,6 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override @Override
protected void cleanupForComparison(DeviceProfile deviceProfile) { protected void cleanupForComparison(DeviceProfile deviceProfile) {
super.cleanupForComparison(deviceProfile); super.cleanupForComparison(deviceProfile);
deviceProfile.setFirmwareId(null);
deviceProfile.setSoftwareId(null);
} }
@Override @Override

76
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java

@ -0,0 +1,76 @@
/**
* 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.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageImportService extends BaseEntityImportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
private final OtaPackageService otaPackageService;
@Override
protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) {
otaPackage.setTenantId(tenantId);
}
@Override
protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) {
otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId()));
return otaPackage;
}
@Override
protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) {
OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider);
if (existingOtaPackage == null && ctx.isFindExistingByName()) {
existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitleAndVersion(ctx.getTenantId(), otaPackage.getTitle(), otaPackage.getVersion());
}
return existingOtaPackage;
}
@Override
protected OtaPackage deepCopy(OtaPackage otaPackage) {
return new OtaPackage(otaPackage);
}
@Override
protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (otaPackage.hasUrl()) {
OtaPackageInfo info = new OtaPackageInfo(otaPackage);
return new OtaPackage(otaPackageService.saveOtaPackageInfo(info, info.hasUrl()));
}
return otaPackageService.saveOtaPackage(otaPackage);
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;
}
}

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

@ -323,7 +323,7 @@ cassandra:
poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
# Interval in milliseconds for printing Cassandra query queue statistic # Interval in milliseconds for printing Cassandra query queue statistic
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data type values except target to null for the same ts on save # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timestamp).
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}" set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled) # log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
@ -656,9 +656,9 @@ cache:
trendzSettings: trendzSettings:
timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL
maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
aiModelSettings: aiModel:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above # Deliberately placed outside the 'specs' group above
notificationRules: notificationRules:
@ -874,7 +874,7 @@ audit-log:
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels.
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels.
"calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels.
"ai_model_settings": "${AUDIT_LOG_MASK_AI_MODEL_SETTINGS:W}" # AI model settings logging levels. "ai_model": "${AUDIT_LOG_MASK_AI_MODEL:W}" # AI model logging levels.
sink: sink:
# Type of external sink. possible options: none, elasticsearch # Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}" type: "${AUDIT_LOG_SINK_TYPE:none}"
@ -1673,7 +1673,7 @@ queue:
# Kafka properties for Notifications topics # Kafka properties for Notifications topics
notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for JS Executor topics # Kafka properties for JS Executor topics
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600;partitions:30;min.insync.replicas:1}"
# Kafka properties for OTA updates topic # Kafka properties for OTA updates topic
ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Version Control topic # Kafka properties for Version Control topic

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

@ -202,6 +202,60 @@ public class TbResourceControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName()); Assert.assertEquals(savedResource.getFileName(), foundResource.getFileName());
} }
@Test
public void testFindSystemResourceInfoById() throws Exception {
loginSysAdmin();
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("My system resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setEncodedData(TEST_DATA);
TbResourceInfo savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME);
TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginTenantAdmin();
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginSysAdmin();
resource = new TbResource(savedResourceInfo);
resource.setFileName(DEFAULT_FILE_NAME_2);
resource.setEncodedData(TEST_DATA);
savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2);
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
loginTenantAdmin();
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
}
@Test
public void testFindTenantResourceInfoById() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JS_MODULE);
resource.setTitle("My tenant resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setEncodedData(TEST_DATA);
TbResourceInfo savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME);
TbResourceInfo resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
resource = new TbResource(savedResourceInfo);
resource.setFileName(DEFAULT_FILE_NAME_2);
resource.setEncodedData(TEST_DATA);
savedResourceInfo = save(resource);
assertThat(savedResourceInfo.getFileName()).isEqualTo(DEFAULT_FILE_NAME_2);
resourceInfo = findResourceInfo(savedResourceInfo.getId());
assertThat(resourceInfo).isEqualTo(savedResourceInfo);
}
@Test @Test
public void testDeleteTbResource() throws Exception { public void testDeleteTbResource() throws Exception {
TbResource resource = new TbResource(); TbResource resource = new TbResource();
@ -878,6 +932,10 @@ public class TbResourceControllerTest extends AbstractControllerTest {
}); });
} }
private TbResourceInfo findResourceInfo(TbResourceId id) throws Exception {
return doGet("/api/resource/info/" + id, TbResourceInfo.class);
}
private byte[] download(TbResourceId resourceId) throws Exception { private byte[] download(TbResourceId resourceId) throws Exception {
return doGet("/api/resource/" + resourceId + "/download") return doGet("/api/resource/" + resourceId + "/download")
.andExpect(status().isOk()) .andExpect(status().isOk())

13
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -75,6 +75,7 @@ import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeEventService;
@ -565,7 +566,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception { protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create device and assign to edge // create device and assign to edge
Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName());
edgeImitator.expectMessageAmount(2); // device and device profile messages DeviceCredentials deviceCredentials = doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
edgeImitator.expectMessageAmount(3); // device and device profile messages and device credentials
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/device/" + savedDevice.getUuidId(), Device.class); + "/device/" + savedDevice.getUuidId(), Device.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
@ -582,6 +584,15 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType());
Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); Assert.assertEquals(thermostatDeviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());
Optional<DeviceCredentialsUpdateMsg> deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsUpdateMsg.class);
Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent());
DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = deviceCredentialsUpdateMsgOpt.get();
DeviceCredentials deviceCredentialsMsg = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true);
Assert.assertNotNull(deviceCredentialsMsg);
Assert.assertEquals(savedDevice.getId(), deviceCredentialsMsg.getDeviceId());
Assert.assertEquals(deviceCredentials, deviceCredentialsMsg);
return savedDevice; return savedDevice;
} }

267
application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java

@ -0,0 +1,267 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edge;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldRequestMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class CalculatedFieldEdgeTest extends AbstractEdgeTest {
private static final String DEFAULT_CF_NAME = "Edge Test CalculatedField";
private static final String UPDATED_CF_NAME = "Updated Edge Test CalculatedField";
@Test
public void testCalculatedField_create_update_delete() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertEquals(savedDevice.getId(), calculatedFieldFromMsg.getEntityId());
Assert.assertEquals(config, calculatedFieldFromMsg.getConfiguration());
edgeImitator.expectMessageAmount(1);
savedCalculatedField.setName(UPDATED_CF_NAME);
savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
calculatedFieldFromMsg = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(UPDATED_CF_NAME, calculatedFieldFromMsg.getName());
// delete calculatedField
edgeImitator.expectMessageAmount(1);
doDelete("/api/calculatedField/" + savedCalculatedField.getUuidId())
.andExpect(status().isOk());
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
Assert.assertEquals(savedCalculatedField.getUuidId().getMostSignificantBits(), calculatedFieldUpdateMsg.getIdMSB());
Assert.assertEquals(savedCalculatedField.getUuidId().getLeastSignificantBits(), calculatedFieldUpdateMsg.getIdLSB());
}
@Test
public void testSendCalculatedFieldToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testSendCalculatedFieldRequestToCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldRequestMsg.Builder calculatedFieldRequestMsgBuilder = CalculatedFieldRequestMsg.newBuilder();
calculatedFieldRequestMsgBuilder.setEntityIdMSB(savedDevice.getId().getId().getMostSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityIdLSB(savedDevice.getId().getId().getLeastSignificantBits());
calculatedFieldRequestMsgBuilder.setEntityType(savedDevice.getId().getEntityType().name());
testAutoGeneratedCodeByProtobuf(calculatedFieldRequestMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldRequestMsg(calculatedFieldRequestMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof CalculatedFieldUpdateMsg);
CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = (CalculatedFieldUpdateMsg) latestMessage;
CalculatedField calculatedFieldFromEdge = JacksonUtil.fromString(calculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromEdge);
Assert.assertEquals(savedCalculatedField, calculatedFieldFromEdge);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, calculatedFieldUpdateMsg.getMsgType());
}
@Test
public void testUpdateCalculatedFieldNameOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(uplinkMsg, uuid, calculatedField.getName());
calculatedField.setName(UPDATED_CF_NAME);
UplinkMsg updatedUplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE);
checkCalculatedFieldOnCloud(updatedUplinkMsg, uuid, calculatedField.getName());
}
@Test
public void testCalculatedFieldToCloudWithNameThatAlreadyExistsOnCloud() throws Exception {
Device savedDevice = saveDeviceOnCloudAndVerifyDeliveryToEdge();
// create calculatedField
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
CalculatedField calculatedField = createSimpleCalculatedField(savedDevice.getId(), config);
edgeImitator.expectMessageAmount(1);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class);
Assert.assertTrue(edgeImitator.waitForMessages());
UUID uuid = Uuids.timeBased();
UplinkMsg uplinkMsg = getUplinkMsg(uuid, calculatedField, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE);
edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<CalculatedFieldUpdateMsg> calculatedFieldUpdateMsgOpt = edgeImitator.findMessageByType(CalculatedFieldUpdateMsg.class);
Assert.assertTrue(calculatedFieldUpdateMsgOpt.isPresent());
CalculatedFieldUpdateMsg latestCalculatedFieldUpdateMsg = calculatedFieldUpdateMsgOpt.get();
CalculatedField calculatedFieldFromMsg = JacksonUtil.fromString(latestCalculatedFieldUpdateMsg.getEntity(), CalculatedField.class, true);
Assert.assertNotNull(calculatedFieldFromMsg);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromMsg.getName());
Assert.assertNotEquals(savedCalculatedField.getUuidId(), uuid);
CalculatedField calculatedFieldFromCloud = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedFieldFromCloud);
Assert.assertNotEquals(DEFAULT_CF_NAME, calculatedFieldFromCloud.getName());
}
private CalculatedField createSimpleCalculatedField(EntityId entityId, SimpleCalculatedFieldConfiguration config) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setTenantId(tenantId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName(DEFAULT_CF_NAME);
calculatedField.setDebugSettings(DebugSettings.all());
Argument argument = new Argument();
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
argument.setDefaultValue("12"); // not used because real telemetry value in db is present
config.setArguments(Map.of("T", argument));
config.setExpression("(T * 9/5) + 32");
Output output = new Output();
output.setName("fahrenheitTemp");
output.setType(OutputType.TIME_SERIES);
output.setDecimalsByDefault(2);
config.setOutput(output);
calculatedField.setConfiguration(config);
return calculatedField;
}
private UplinkMsg getUplinkMsg(UUID uuid, CalculatedField calculatedField, UpdateMsgType updateMsgType) throws InvalidProtocolBufferException {
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
CalculatedFieldUpdateMsg.Builder calculatedFieldUpdateMsgBuilder = CalculatedFieldUpdateMsg.newBuilder();
calculatedFieldUpdateMsgBuilder.setIdMSB(uuid.getMostSignificantBits());
calculatedFieldUpdateMsgBuilder.setIdLSB(uuid.getLeastSignificantBits());
calculatedFieldUpdateMsgBuilder.setEntity(JacksonUtil.toString(calculatedField));
calculatedFieldUpdateMsgBuilder.setMsgType(updateMsgType);
testAutoGeneratedCodeByProtobuf(calculatedFieldUpdateMsgBuilder);
uplinkMsgBuilder.addCalculatedFieldUpdateMsg(calculatedFieldUpdateMsgBuilder.build());
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
return uplinkMsgBuilder.build();
}
private void checkCalculatedFieldOnCloud(UplinkMsg uplinkMsg, UUID uuid, String resourceTitle) throws Exception {
edgeImitator.expectResponsesAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsg);
Assert.assertTrue(edgeImitator.waitForResponses());
UplinkResponseMsg latestResponseMsg = edgeImitator.getLatestResponseMsg();
Assert.assertTrue(latestResponseMsg.getSuccess());
CalculatedField calculatedField = doGet("/api/calculatedField/" + uuid, CalculatedField.class);
Assert.assertNotNull(calculatedField);
Assert.assertEquals(resourceTitle, calculatedField.getName());
}
}

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

@ -33,6 +33,7 @@ import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
@ -352,6 +353,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(notificationTargetUpdateMsg)); result.add(saveDownlinkMsg(notificationTargetUpdateMsg));
} }
} }
if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) { if (downlinkMsg.hasEdgeConfiguration()) {
result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration())); result.add(saveDownlinkMsg(downlinkMsg.getEdgeConfiguration()));
} }

4
application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java

@ -2451,7 +2451,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
public void isInsidePolygon_Test() throws ExecutionException, InterruptedException { public void isInsidePolygon_Test() throws ExecutionException, InterruptedException {
msgStr = "{}"; msgStr = "{}";
decoderStr = """ decoderStr = """
String perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]"; var perimeter = "[[[37.7810,-122.4210],[37.7890,-122.3900],[37.7700,-122.3800],[37.7600,-122.4000],[37.7700,-122.4250],[37.7810,-122.4210]],[[37.7730,-122.4050],[37.7700,-122.3950],[37.7670,-122.3980],[37.7690,-122.4100],[37.7730,-122.4050]]]";
return{ return{
outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter), outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter),
insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter), insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter),
@ -2470,7 +2470,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
public void isInsideCircle_Test() throws ExecutionException, InterruptedException { public void isInsideCircle_Test() throws ExecutionException, InterruptedException {
msgStr = "{}"; msgStr = "{}";
decoderStr = """ decoderStr = """
String perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}"; var perimeter = "{\\"latitude\\":37.7749,\\"longitude\\":-122.4194,\\"radius\\":3000,\\"radiusUnit\\":\\"METER\\"}";
return{ return{
outsideCircle: isInsideCircle(37.8044, -122.2712, perimeter), outsideCircle: isInsideCircle(37.8044, -122.2712, perimeter),
insideCircle: isInsideCircle(37.7599, -122.4148, perimeter) insideCircle: isInsideCircle(37.7599, -122.4148, perimeter)

29
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -66,6 +66,7 @@ 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.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
@ -203,11 +204,12 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1"); AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1", firmware.getId(), null);
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId()); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId(), firmware.getId())
.map(entityId -> { .map(entityId -> {
try { try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
@ -275,12 +277,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any()); verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedDeviceProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED), any());
verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false)); verify(otaPackageStateService).update(eq(importedDeviceProfile), eq(false), eq(false));
OtaPackage importedFirmware = (OtaPackage) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.OTA_PACKAGE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedFirmware.getId()), eq(importedFirmware),
any(), eq(ActionType.ADDED), isNull());
Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity(); Device importedDevice = (Device) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice), verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice),
any(), eq(ActionType.ADDED), isNull()); any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull()); verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull());
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE)); importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice)); verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId());
// calculated field of imported device: // calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId()); List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assetProfile = assetProfileService.saveAssetProfile(assetProfile); assetProfile = assetProfileService.saveAssetProfile(assetProfile);
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1"); DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1", firmware.getId(), null);
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1"); EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId()); CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>(); Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(), for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) { deviceProfile.getId(), firmware.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId); EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder() EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false) .saveCredentials(false)
@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId()); assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId()); assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())); OtaPackage exportedFirmware = (OtaPackage) exportEntity(tenantAdmin2, (OtaPackageId) ids.get(firmware.getId())).getEntity();
assertThat(exportedFirmware.getDeviceProfileId()).isEqualTo(exportedDeviceProfile.getId());
assertThat(exportedFirmware.getId()).isEqualTo(firmware.getId());
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity(); Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId()); assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId()); assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
assertThat(exportedDevice.getFirmwareId()).isEqualTo(firmware.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields(); List<CalculatedField> calculatedFields = entityExportData.getCalculatedFields();
assertThat(calculatedFields.size()).isOne(); assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0); CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName()); assertThat(field.getName()).isEqualTo(calculatedField.getName());
@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile); deviceProfileService.saveDeviceProfile(importedDeviceProfile);
} }
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) { protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name, OtaPackageId firmwareId, OtaPackageId softwareId) {
Device device = new Device(); Device device = new Device();
device.setTenantId(tenantId); device.setTenantId(tenantId);
device.setCustomerId(customerId); device.setCustomerId(customerId);
device.setName(name); device.setName(name);
device.setLabel("lbl"); device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId); device.setDeviceProfileId(deviceProfileId);
device.setFirmwareId(firmwareId);
device.setSoftwareId(softwareId);
DeviceData deviceData = new DeviceData(); DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
device.setDeviceData(deviceData); device.setDeviceData(deviceData);

80
application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java

@ -116,8 +116,8 @@ import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
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.controller.TbResourceControllerTest.TEST_DATA;
import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME; import static org.thingsboard.server.controller.TbResourceControllerTest.JS_TEST_FILE_NAME;
import static org.thingsboard.server.controller.TbResourceControllerTest.TEST_DATA;
@DaoSqlTest @DaoSqlTest
public class VersionControlTest extends AbstractControllerTest { public class VersionControlTest extends AbstractControllerTest {
@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest {
} }
@Test @Test
public void testDeviceVc_withProfile_betweenTenants() throws Exception { public void testDeviceVc_withProfileAndOtaPackage_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1");
createVersion("profiles", EntityType.DEVICE_PROFILE); createVersion("profiles", EntityType.DEVICE_PROFILE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1"); OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
String versionId = createVersion("devices", EntityType.DEVICE); OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> {
newDevice.setFirmwareId(firmware.getId());
newDevice.setSoftwareId(software.getId());
});
String versionId = createVersion("devices with ota", EntityType.DEVICE, EntityType.OTA_PACKAGE);
DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId()); DeviceCredentials deviceCredentials = findDeviceCredentials(device.getId());
DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials); DeviceCredentials newCredentials = new DeviceCredentials(deviceCredentials);
newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import
doPost("/api/device/credentials", newCredentials, DeviceCredentials.class); doPost("/api/device/credentials", newCredentials, DeviceCredentials.class);
assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices", "profiles"); assertThat(listVersions()).extracting(EntityVersion::getName).containsExactly("devices with ota", "profiles");
loginTenant2(); loginTenant2();
Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE); Map<EntityType, EntityTypeLoadResult> result = loadVersion(versionId, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1);
assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1);
@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId()); assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId());
assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue()); assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue());
assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType()); assertThat(importedCredentials.getCredentialsType()).isEqualTo(deviceCredentials.getCredentialsType());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
} }
@Test @Test
@ -653,6 +665,57 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
} }
@Test
public void testOtaPackageVc_sameTenant() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
String versionId = createVersion("ota packages", EntityType.OTA_PACKAGE);
OtaPackage firmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage softwareOta = findOtaPackage(software.getTitle());
loadVersion(versionId, EntityType.OTA_PACKAGE);
OtaPackage importedFirmwareOta = findOtaPackage(firmwareOta.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(softwareOta.getTitle());
checkImportedEntity(tenantId1, firmwareOta, tenantId1, importedFirmwareOta);
checkImportedOtaPackageData(firmwareOta, importedFirmwareOta);
checkImportedEntity(tenantId1, softwareOta, tenantId1, importedSoftwareOta);
checkImportedOtaPackageData(softwareOta, importedSoftwareOta);
}
@Test
public void testOtaPackageVcWithProfile_betweenTenants() throws Exception {
DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile v1.0");
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
deviceProfile.setFirmwareId(firmware.getId());
deviceProfile.setSoftwareId(software.getId());
deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
String versionId = createVersion("ota packages", EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
loginTenant2();
loadVersion(versionId, EntityType.DEVICE_PROFILE, EntityType.OTA_PACKAGE);
DeviceProfile importedProfile = findDeviceProfile(deviceProfile.getName());
OtaPackage importedFirmwareOta = findOtaPackage(firmware.getTitle());
OtaPackage importedSoftwareOta = findOtaPackage(software.getTitle());
checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedProfile);
checkImportedDeviceProfileData(deviceProfile, importedProfile);
checkImportedEntity(tenantId1, firmware, tenantId2, importedFirmwareOta);
checkImportedOtaPackageData(firmware, importedFirmwareOta);
checkImportedEntity(tenantId1, software, tenantId2, importedSoftwareOta);
checkImportedOtaPackageData(software, importedSoftwareOta);
assertThat(importedProfile.getFirmwareId()).isEqualTo(importedFirmwareOta.getId());
assertThat(importedProfile.getSoftwareId()).isEqualTo(importedSoftwareOta.getId());
}
protected void checkImportedOtaPackageData(OtaPackage otaPackage, OtaPackage importedOtaPackage) {
assertThat(importedOtaPackage.getName()).isEqualTo(otaPackage.getName());
assertThat(importedOtaPackage.getTag()).isEqualTo(otaPackage.getTag());
assertThat(importedOtaPackage.getType()).isEqualTo(otaPackage.getType());
assertThat(importedOtaPackage.getFileName()).isEqualTo(otaPackage.getFileName());
}
@Test @Test
public void testResourceVc_sameTenant() throws Exception { public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource"); TbResourceInfo resourceInfo = createResource("Test resource");
@ -923,6 +986,7 @@ public class VersionControlTest extends AbstractControllerTest {
otaPackage.setDeviceProfileId(deviceProfileId); otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type); otaPackage.setType(type);
otaPackage.setTitle("My " + type); otaPackage.setTitle("My " + type);
otaPackage.setTag("My " + type);
otaPackage.setVersion("v1.0"); otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt"); otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain"); otaPackage.setContentType("text/plain");
@ -933,6 +997,10 @@ public class VersionControlTest extends AbstractControllerTest {
return otaPackageService.saveOtaPackage(otaPackage); return otaPackageService.saveOtaPackage(otaPackage);
} }
private OtaPackage findOtaPackage(String title) throws Exception {
return doGetTypedWithPageLink("/api/otaPackages?", new TypeReference<PageData<OtaPackage>>() {}, new PageLink(100, 0, title)).getData().get(0);
}
protected Dashboard createDashboard(CustomerId customerId, String name) { protected Dashboard createDashboard(CustomerId customerId, String name) {
Dashboard dashboard = new Dashboard(); Dashboard dashboard = new Dashboard();
dashboard.setTitle(name); dashboard.setTitle(name);

4
common/cache/src/main/java/org/thingsboard/server/cache/resourceInfo/ResourceInfoCacheKey.java

@ -20,7 +20,6 @@ import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
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 java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 2100510964692846992L; private static final long serialVersionUID = 2100510964692846992L;
private final TenantId tenantId;
private final TbResourceId tbResourceId; private final TbResourceId tbResourceId;
@Override @Override
public String toString() { public String toString() {
return tenantId + "_" + tbResourceId; return tbResourceId.toString();
} }
} }

7
common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java

@ -17,11 +17,12 @@ package org.thingsboard.server.queue;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
/**
* Created by ashvayka on 05.10.18.
*/
public interface TbQueueHandler<Request extends TbQueueMsg, Response extends TbQueueMsg> { public interface TbQueueHandler<Request extends TbQueueMsg, Response extends TbQueueMsg> {
ListenableFuture<Response> handle(Request request); ListenableFuture<Response> handle(Request request);
default Response constructErrorResponseMsg(Request request, Throwable cause) {
return null;
}
} }

18
common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelSettingsService.java → common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java

@ -16,8 +16,8 @@
package org.thingsboard.server.dao.ai; package org.thingsboard.server.dao.ai;
import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
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;
@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Optional; import java.util.Optional;
public interface AiModelSettingsService extends EntityDaoService { public interface AiModelService extends EntityDaoService {
AiModelSettings save(AiModelSettings settings); AiModel save(AiModel model);
Optional<AiModelSettings> findAiModelSettingsById(TenantId tenantId, AiModelSettingsId settingsId); Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
PageData<AiModelSettings> findAiModelSettingsByTenantId(TenantId tenantId, PageLink pageLink); PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
Optional<AiModelSettings> findAiModelSettingsByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); Optional<AiModel> findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId);
FluentFuture<Optional<AiModelSettings>> findAiModelSettingsByTenantIdAndIdAsync(TenantId tenantId, AiModelSettingsId settingsId); FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelSettingsId settingsId); boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
} }

4
common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java

@ -31,8 +31,12 @@ public interface CalculatedFieldService extends EntityDaoService {
CalculatedField save(CalculatedField calculatedField); CalculatedField save(CalculatedField calculatedField);
CalculatedField save(CalculatedField calculatedField, boolean doValidate);
CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId); CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
CalculatedField findByEntityIdAndName(EntityId entityId, String name);
List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId); List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedField> findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId); List<CalculatedField> findCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java

@ -41,6 +41,8 @@ public interface OtaPackageService extends EntityDaoService {
OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId); OtaPackageInfo findOtaPackageInfoById(TenantId tenantId, OtaPackageId otaPackageId);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId); ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId);
PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink);
@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService {
void deleteOtaPackagesByTenantId(TenantId tenantId); void deleteOtaPackagesByTenantId(TenantId tenantId);
long sumDataSizeByTenantId(TenantId tenantId); long sumDataSizeByTenantId(TenantId tenantId);
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data;
public final class CacheConstants { public final class CacheConstants {
private CacheConstants() {}
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations"; public static final String RELATIONS_CACHE = "relations";
public static final String DEVICE_CACHE = "devices"; public static final String DEVICE_CACHE = "devices";
@ -37,7 +39,7 @@ public final class CacheConstants {
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings";
public static final String AI_MODEL_SETTINGS_CACHE = "aiModelSettings"; public static final String AI_MODEL_CACHE = "aiModel";
public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes"; public static final String ATTRIBUTES_CACHE = "attributes";

4
common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java

@ -66,10 +66,10 @@ public enum EntityType {
CALCULATED_FIELD(39), CALCULATED_FIELD(39),
CALCULATED_FIELD_LINK(40), CALCULATED_FIELD_LINK(40),
JOB(41), JOB(41),
AI_MODEL_SETTINGS(42, "ai_model_settings") { AI_MODEL(42, "ai_model") {
@Override @Override
public String getNormalName() { public String getNormalName() {
return "AI model settings"; return "AI model";
} }
}; };

8
common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java

@ -20,6 +20,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.OtaPackageId;
import java.io.Serial;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@Schema @Schema
@ -27,6 +28,7 @@ import java.nio.ByteBuffer;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class OtaPackage extends OtaPackageInfo { public class OtaPackage extends OtaPackageInfo {
@Serial
private static final long serialVersionUID = 3091601761339422546L; private static final long serialVersionUID = 3091601761339422546L;
@Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY)
@ -44,4 +46,10 @@ public class OtaPackage extends OtaPackageInfo {
super(otaPackage); super(otaPackage);
this.data = otaPackage.getData(); this.data = otaPackage.getData();
} }
public OtaPackage(OtaPackageInfo otaPackageInfo) {
super(otaPackageInfo);
this.data = null;
}
} }

12
common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@ -29,12 +30,15 @@ import org.thingsboard.server.common.data.ota.OtaPackageType;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema @Schema
@Slf4j @Slf4j
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle { public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> implements HasName, HasTenantId, HasTitle, ExportableEntity<OtaPackageId> {
@Serial
private static final long serialVersionUID = 3168391583570815419L; private static final long serialVersionUID = 3168391583570815419L;
@Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with Tenant Id. Tenant Id of the ota package can't be changed.", accessMode = Schema.AccessMode.READ_ONLY)
@ -77,6 +81,8 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
@Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "OTA Package data size.", example = "8", accessMode = Schema.AccessMode.READ_ONLY)
private Long dataSize; private Long dataSize;
private OtaPackageId externalId;
public OtaPackageInfo() { public OtaPackageInfo() {
super(); super();
} }
@ -100,6 +106,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm(); this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm();
this.checksum = otaPackageInfo.getChecksum(); this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize(); this.dataSize = otaPackageInfo.getDataSize();
this.externalId = otaPackageInfo.getExternalId();
} }
@Schema(description = "JSON object with the ota package Id. " + @Schema(description = "JSON object with the ota package Id. " +
@ -118,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
} }
@Override @Override
@JsonIgnore @JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() { public String getName() {
return title; return title;
} }
@ -133,4 +140,5 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
public JsonNode getAdditionalInfo() { public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo(); return super.getAdditionalInfo();
} }
} }

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

@ -23,6 +23,7 @@ import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import java.io.Serial;
import java.util.Base64; import java.util.Base64;
import java.util.Optional; import java.util.Optional;
@ -31,6 +32,7 @@ import java.util.Optional;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class TbResource extends TbResourceInfo { public class TbResource extends TbResourceInfo {
@Serial
private static final long serialVersionUID = 7379609705527272306L; private static final long serialVersionUID = 7379609705527272306L;
private byte[] data; private byte[] data;
@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo {
public String toString() { public String toString() {
return super.toString(); return super.toString();
} }
} }

38
common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModelSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java

@ -27,8 +27,8 @@ import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ai.model.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoNullChar;
@ -39,7 +39,7 @@ import java.io.Serial;
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public final class AiModelSettings extends BaseData<AiModelSettingsId> implements HasTenantId, HasVersion, ExportableEntity<AiModelSettingsId> { public final class AiModel extends BaseData<AiModelId> implements HasTenantId, HasVersion, ExportableEntity<AiModelId> {
@Serial @Serial
private static final long serialVersionUID = 9017108678716011604L; private static final long serialVersionUID = 9017108678716011604L;
@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY, accessMode = Schema.AccessMode.READ_ONLY,
description = "JSON object representing the ID of the tenant associated with these AI model settings", description = "JSON object representing the ID of the tenant associated with this AI model",
example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d" example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d"
) )
private TenantId tenantId; private TenantId tenantId;
@ -55,7 +55,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY, accessMode = Schema.AccessMode.READ_ONLY,
description = "Version of the AI model settings; increments automatically whenever the settings are changed", description = "Version of the AI model record; increments automatically whenever the record is changed",
example = "7", example = "7",
defaultValue = "1" defaultValue = "1"
) )
@ -67,8 +67,8 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,
description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant", description = "Display name for this AI model configuration; not the technical model identifier",
example = "Rule node assistant" example = "Fast and cost-efficient model"
) )
private String name; private String name;
@ -79,24 +79,24 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
accessMode = Schema.AccessMode.READ_WRITE, accessMode = Schema.AccessMode.READ_WRITE,
description = "Configuration of the AI model" description = "Configuration of the AI model"
) )
private AiModel<?> configuration; private AiModelConfig configuration;
private AiModelSettingsId externalId; private AiModelId externalId;
public AiModelSettings() {} public AiModel() {}
public AiModelSettings(AiModelSettingsId id) { public AiModel(AiModelId id) {
super(id); super(id);
} }
public AiModelSettings(AiModelSettings settings) { public AiModel(AiModel model) {
super(settings.getId()); super(model.getId());
createdTime = settings.getCreatedTime(); createdTime = model.getCreatedTime();
tenantId = settings.getTenantId(); tenantId = model.getTenantId();
version = settings.getVersion(); version = model.getVersion();
name = settings.getName(); name = model.getName();
configuration = settings.getConfiguration(); configuration = model.getConfiguration();
externalId = settings.getExternalId() == null ? null : new AiModelSettingsId(settings.getExternalId().getId()); externalId = model.getExternalId() == null ? null : new AiModelId(model.getExternalId().getId());
} }
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatRequest.java

@ -23,7 +23,7 @@ import dev.langchain4j.model.chat.request.ChatRequest;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModel; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -51,7 +51,7 @@ public record TbChatRequest(
description = "Configuration of the AI chat model that should execute the request" description = "Configuration of the AI chat model that should execute the request"
) )
@NotNull @Valid @NotNull @Valid
AiChatModel<?> chatModel AiChatModelConfig<?> chatModelConfig
) { ) {
public ChatRequest toLangChainChatRequest() { public ChatRequest toLangChainChatRequest() {

84
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModel.java

@ -1,84 +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.ai.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModel;
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModel;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModel;
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.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
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.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "provider",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiChatModel.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiChatModel.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiChatModel.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModel.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiChatModel.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModel.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModel.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModel.class, name = "GITHUB_MODELS")
})
public interface AiModel<C extends AiModelConfig> {
AiProvider provider();
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "provider"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS")
})
AiProviderConfig providerConfig();
@JsonProperty("modelType")
AiModelType modelType();
C modelConfig();
AiModel<C> withModelConfig(C config);
}

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

@ -15,8 +15,66 @@
*/ */
package org.thingsboard.server.common.data.ai.model; package org.thingsboard.server.common.data.ai.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
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.AzureOpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelConfig;
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.MistralAiChatModelConfig;
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.AiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
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.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "provider",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiChatModelConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiChatModelConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS")
})
public interface AiModelConfig { public interface AiModelConfig {
String modelId(); AiProvider provider();
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "provider"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = AzureOpenAiProviderConfig.class, name = "AZURE_OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = GoogleVertexAiGeminiProviderConfig.class, name = "GOOGLE_VERTEX_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS")
})
AiProviderConfig providerConfig();
@JsonProperty("modelType")
AiModelType modelType();
} }

41
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModel.java

@ -1,41 +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.ai.model.chat;
import dev.langchain4j.model.chat.ChatModel;
import org.thingsboard.server.common.data.ai.model.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModelType;
public sealed interface AiChatModel<C extends AiChatModelConfig<C>> extends AiModel<C>
permits
OpenAiChatModel, AzureOpenAiChatModel, GoogleAiGeminiChatModel,
GoogleVertexAiGeminiChatModel, MistralAiChatModel, AnthropicChatModel,
AmazonBedrockChatModel, GitHubModelsChatModel {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
@Override
C modelConfig();
@Override
AiChatModel<C> withModelConfig(C config);
}

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

@ -15,13 +15,22 @@
*/ */
package org.thingsboard.server.common.data.ai.model.chat; package org.thingsboard.server.common.data.ai.model.chat;
import dev.langchain4j.model.chat.ChatModel;
import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.ai.model.AiModelType;
public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extends AiModelConfig public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extends AiModelConfig
permits permits
OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config, OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config { AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
Integer timeoutSeconds(); Integer timeoutSeconds();

21
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

@ -27,27 +27,22 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
public record AmazonBedrockChatModel( public record AmazonBedrockChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AmazonBedrockProviderConfig providerConfig, @NotNull @Valid AmazonBedrockProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AmazonBedrockChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.AMAZON_BEDROCK; return AiProvider.AMAZON_BEDROCK;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

23
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java

@ -27,28 +27,23 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.AnthropicProviderConfig; import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
public record AnthropicChatModel( public record AnthropicChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AnthropicProviderConfig providerConfig, @NotNull @Valid AnthropicProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AnthropicChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.ANTHROPIC; return AiProvider.ANTHROPIC;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
public record AzureOpenAiChatModel( public record AzureOpenAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid AzureOpenAiProviderConfig providerConfig, @NotNull @Valid AzureOpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<AzureOpenAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.AZURE_OPENAI; return AiProvider.AZURE_OPENAI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.GitHubModelsProviderConfig; import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig;
public record GitHubModelsChatModel( public record GitHubModelsChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GitHubModelsProviderConfig providerConfig, @NotNull @Valid GitHubModelsProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GitHubModelsChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GITHUB_MODELS; return AiProvider.GITHUB_MODELS;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

27
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java

@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
public record GoogleAiGeminiChatModel( public record GoogleAiGeminiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GoogleAiGeminiProviderConfig providerConfig, @NotNull @Valid GoogleAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GoogleAiGeminiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GOOGLE_AI_GEMINI; return AiProvider.GOOGLE_AI_GEMINI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

27
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java

@ -27,30 +27,25 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
public record GoogleVertexAiGeminiChatModel( public record GoogleVertexAiGeminiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig, @NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<GoogleVertexAiGeminiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.GOOGLE_VERTEX_AI_GEMINI; return AiProvider.GOOGLE_VERTEX_AI_GEMINI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer topK,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

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

@ -19,20 +19,20 @@ import dev.langchain4j.model.chat.ChatModel;
public interface Langchain4jChatModelConfigurer { public interface Langchain4jChatModelConfigurer {
ChatModel configureChatModel(OpenAiChatModel chatModel); ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(AzureOpenAiChatModel chatModel); ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel); ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig);
ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel); ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig);
ChatModel configureChatModel(MistralAiChatModel chatModel); ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig);
ChatModel configureChatModel(AnthropicChatModel chatModel); ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig);
ChatModel configureChatModel(AmazonBedrockChatModel chatModel); ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig);
ChatModel configureChatModel(GitHubModelsChatModel chatModel); ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig);
} }

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.MistralAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
public record MistralAiChatModel( public record MistralAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid MistralAiProviderConfig providerConfig, @NotNull @Valid MistralAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<MistralAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.MISTRAL_AI; return AiProvider.MISTRAL_AI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

25
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModel.java → common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java

@ -27,29 +27,24 @@ import org.thingsboard.server.common.data.ai.model.AiModelType;
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.OpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
public record OpenAiChatModel( public record OpenAiChatModelConfig(
AiModelType modelType, AiModelType modelType,
@NotNull @Valid OpenAiProviderConfig providerConfig, @NotNull @Valid OpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig @NotBlank String modelId,
) implements AiChatModel<OpenAiChatModel.Config> { @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModelConfig> {
@Override @Override
public AiProvider provider() { public AiProvider provider() {
return AiProvider.OPENAI; return AiProvider.OPENAI;
} }
@With
public record Config(
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
Double frequencyPenalty,
Double presencePenalty,
@Positive Integer maxOutputTokens,
@Positive Integer timeoutSeconds,
@PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModel.Config> {}
@Override @Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) { public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this); return configurer.configureChatModel(this);

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

@ -41,12 +41,13 @@ public enum EdgeEventType {
ADMIN_SETTINGS(true, null), ADMIN_SETTINGS(true, null),
OTA_PACKAGE(true, EntityType.OTA_PACKAGE), OTA_PACKAGE(true, EntityType.OTA_PACKAGE),
QUEUE(true, EntityType.QUEUE), QUEUE(true, EntityType.QUEUE),
NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE), NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE),
NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET), NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET),
NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE), NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE),
TB_RESOURCE(true, EntityType.TB_RESOURCE), TB_RESOURCE(true, EntityType.TB_RESOURCE),
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT), OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN); DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD);
private final boolean allEdgesRelated; private final boolean allEdgesRelated;

16
common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelSettingsId.java → common/data/src/main/java/org/thingsboard/server/common/data/id/AiModelId.java

@ -23,29 +23,29 @@ import org.thingsboard.server.common.data.EntityType;
import java.io.Serial; import java.io.Serial;
import java.util.UUID; import java.util.UUID;
public final class AiModelSettingsId extends UUIDBased implements EntityId { public final class AiModelId extends UUIDBased implements EntityId {
@Serial @Serial
private static final long serialVersionUID = 3021036138554389754L; private static final long serialVersionUID = 3021036138554389754L;
@JsonCreator @JsonCreator
public AiModelSettingsId(@JsonProperty("id") UUID id) { public AiModelId(@JsonProperty("id") UUID id) {
super(id); super(id);
} }
@Override @Override
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
description = "Entity type of the AI model settings", description = "Entity type of the AI model",
example = "AI_MODEL_SETTINGS", example = "AI_MODEL",
allowableValues = "AI_MODEL_SETTINGS" allowableValues = "AI_MODEL"
) )
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS; return EntityType.AI_MODEL;
} }
public static AiModelSettingsId fromString(String uuid) { public static AiModelId fromString(String uuid) {
return new AiModelSettingsId(UUID.fromString(uuid)); return new AiModelId(UUID.fromString(uuid));
} }
} }

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

@ -85,7 +85,7 @@ public class EntityIdFactory {
case CALCULATED_FIELD -> new CalculatedFieldId(uuid); case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid); case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid);
case JOB -> new JobId(uuid); case JOB -> new JobId(uuid);
case AI_MODEL_SETTINGS -> new AiModelSettingsId(uuid); case AI_MODEL -> new AiModelId(uuid);
default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!"); default -> throw new IllegalArgumentException("EntityType " + type + " is not supported!");
}; };
} }
@ -138,6 +138,8 @@ public class EntityIdFactory {
return new OAuth2ClientId(uuid); return new OAuth2ClientId(uuid);
case DOMAIN: case DOMAIN:
return new DomainId(uuid); return new DomainId(uuid);
case CALCULATED_FIELD:
return new CalculatedFieldId(uuid);
} }
throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!"); throw new IllegalArgumentException("EdgeEventType " + edgeEventType + " is not supported!");
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java

@ -20,10 +20,12 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID; import java.util.UUID;
public class OtaPackageId extends UUIDBased implements EntityId { public class OtaPackageId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@JsonCreator @JsonCreator

12
common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java

@ -77,16 +77,4 @@ public class RateLimitUtil {
return true; return true;
} }
@Deprecated(forRemoval = true, since = "4.1")
public static String deduplicateByDuration(String configStr) {
if (configStr == null) {
return null;
}
Set<Long> distinctDurations = new HashSet<>();
return parseConfig(configStr).stream()
.filter(entry -> distinctDurations.add(entry.durationSeconds()))
.map(RateLimitEntry::toString)
.collect(Collectors.joining(","));
}
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java

@ -25,8 +25,9 @@ import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.ai.AiModelSettings; import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
@ -60,7 +61,8 @@ import java.lang.annotation.Target;
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class), @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class),
@Type(name = "TB_RESOURCE", value = TbResource.class), @Type(name = "TB_RESOURCE", value = TbResource.class),
@Type(name = "AI_MODEL_SETTINGS", value = AiModelSettings.class) @Type(name = "OTA_PACKAGE", value = OtaPackage.class),
@Type(name = "AI_MODEL", value = AiModel.class)
}) })
@JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true) @JsonIgnoreProperties(value = {"tenantId", "createdTime", "version"}, ignoreUnknown = true)
public @interface JsonTbEntity {} public @interface JsonTbEntity {}

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/EntityExportData.java

@ -41,7 +41,8 @@ import java.util.Map;
@Type(name = "DEVICE", value = DeviceExportData.class), @Type(name = "DEVICE", value = DeviceExportData.class),
@Type(name = "RULE_CHAIN", value = RuleChainExportData.class), @Type(name = "RULE_CHAIN", value = RuleChainExportData.class),
@Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class), @Type(name = "WIDGET_TYPE", value = WidgetTypeExportData.class),
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class) @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundleExportData.class),
@Type(name = "OTA_PACKAGE", value = OtaPackageExportData.class)
}) })
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@Data @Data

41
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/OtaPackageExportData.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.sync.ie;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.OtaPackage;
@EqualsAndHashCode(callSuper = true)
public class OtaPackageExportData extends EntityExportData<OtaPackage> {
/*
* OtaPackage is not a versioned entity; its 'version' field is part of the domain model (not used for optimistic locking)
* We override both methods to ensure 'version' is not ignored during (de)serialization.
*/
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public OtaPackage getEntity() {
return super.getEntity();
}
@JsonIgnoreProperties(value = {"tenantId", "createdTime"}, ignoreUnknown = true)
@Override
public void setEntity(OtaPackage entity) {
super.setEntity(entity);
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/request/create/AutoVersionCreateConfig.java

@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.io.Serial;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Data @Data
public class AutoVersionCreateConfig extends VersionCreateConfig { public class AutoVersionCreateConfig extends VersionCreateConfig {
@Serial
private static final long serialVersionUID = 8245450889383315551L; private static final long serialVersionUID = 8245450889383315551L;
private String branch; private String branch;

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

@ -24,7 +24,6 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType; import org.thingsboard.server.common.data.TenantProfileType;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import org.thingsboard.server.common.data.validation.RateLimit; import org.thingsboard.server.common.data.validation.RateLimit;
import java.io.Serial; import java.io.Serial;
@ -236,43 +235,4 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
return maxRuleNodeExecutionsPerMessage; return maxRuleNodeExecutionsPerMessage;
} }
@Deprecated(forRemoval = true, since = "4.1")
public void deduplicateRateLimitsConfigs() {
this.transportTenantMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantMsgRateLimit);
this.transportTenantTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryMsgRateLimit);
this.transportTenantTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryDataPointsRateLimit);
this.transportDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceMsgRateLimit);
this.transportDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryMsgRateLimit);
this.transportDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryDataPointsRateLimit);
this.transportGatewayMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayMsgRateLimit);
this.transportGatewayTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryMsgRateLimit);
this.transportGatewayTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryDataPointsRateLimit);
this.transportGatewayDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceMsgRateLimit);
this.transportGatewayDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryMsgRateLimit);
this.transportGatewayDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryDataPointsRateLimit);
this.tenantEntityExportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityExportRateLimit);
this.tenantEntityImportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityImportRateLimit);
this.tenantNotificationRequestsRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsRateLimit);
this.tenantNotificationRequestsPerRuleRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsPerRuleRateLimit);
this.cassandraReadQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantCoreRateLimits);
this.cassandraWriteQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantCoreRateLimits);
this.cassandraReadQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantRuleEngineRateLimits);
this.cassandraWriteQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantRuleEngineRateLimits);
this.edgeEventRateLimits = RateLimitUtil.deduplicateByDuration(edgeEventRateLimits);
this.edgeEventRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeEventRateLimitsPerEdge);
this.edgeUplinkMessagesRateLimits = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimits);
this.edgeUplinkMessagesRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimitsPerEdge);
this.wsUpdatesPerSessionRateLimit = RateLimitUtil.deduplicateByDuration(wsUpdatesPerSessionRateLimit);
this.tenantServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(tenantServerRestLimitsConfiguration);
this.customerServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(customerServerRestLimitsConfiguration);
}
} }

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

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

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

@ -42,6 +42,7 @@ enum EdgeVersion {
V_3_8_0 = 8; V_3_8_0 = 8;
V_3_9_0 = 9; V_3_9_0 = 9;
V_4_0_0 = 10; V_4_0_0 = 10;
V_4_1_0 = 11;
V_LATEST = 999; V_LATEST = 999;
} }
@ -124,6 +125,14 @@ enum UpdateMsgType {
// use 6 as a next number // use 6 as a next number
} }
message CalculatedFieldUpdateMsg{
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string entity = 4;
}
message EntityDataProto { message EntityDataProto {
int64 entityIdMSB = 1; int64 entityIdMSB = 1;
int64 entityIdLSB = 2; int64 entityIdLSB = 2;
@ -325,6 +334,12 @@ message RelationRequestMsg {
string entityType = 3; string entityType = 3;
} }
message CalculatedFieldRequestMsg {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
string entityType = 3;
}
// DEPRECATED. FOR REMOVAL // DEPRECATED. FOR REMOVAL
message UserCredentialsRequestMsg { message UserCredentialsRequestMsg {
option deprecated = true; option deprecated = true;
@ -423,6 +438,8 @@ message UplinkMsg {
repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22; repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22;
repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23; repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23;
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
} }
message UplinkResponseMsg { message UplinkResponseMsg {
@ -472,4 +489,5 @@ message DownlinkMsg {
repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32; repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32;
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33; repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34; repeated OAuth2DomainUpdateMsg oAuth2DomainUpdateMsg = 34;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 35;
} }

19
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -203,6 +203,25 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
}); });
} }
@Override
public TbProtoQueueMsg<FromEdqsMsg> constructErrorResponseMsg(TbProtoQueueMsg<ToEdqsMsg> request, Throwable e) {
EdqsResponse response = new EdqsResponse();
String errorMessage;
if (e instanceof org.apache.kafka.common.errors.RecordTooLargeException) {
errorMessage = "Result set is too large";
} else if (e instanceof IllegalArgumentException || e instanceof NullPointerException) {
errorMessage = "Invalid request format or missing data: " + ExceptionUtil.getMessage(e);
} else {
errorMessage = ExceptionUtil.getMessage(e);
}
response.setError(errorMessage);
return new TbProtoQueueMsg<>(request.getKey(), FromEdqsMsg.newBuilder()
.setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder()
.setValue(JacksonUtil.toString(response))
.build())
.build(), request.getHeaders());
}
private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) {
EdqsResponse response = new EdqsResponse(); EdqsResponse response = new EdqsResponse();
try { try {

8
common/proto/src/main/java/org/thingsboard/server/common/adaptor/JsonConverter.java

@ -56,11 +56,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509Ce
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -538,13 +536,13 @@ public class JsonConverter {
return result; return result;
} }
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element) { public static List<AttributeKvEntry> convertToAttributes(JsonElement element) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
return convertToAttributes(element, ts); return convertToAttributes(element, ts);
} }
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) { public static List<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) {
return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList()); return parseValues(element.getAsJsonObject()).stream().<AttributeKvEntry>map(kv -> new BaseAttributeKvEntry(kv, ts)).toList();
} }
private static List<KvEntry> parseValues(JsonObject valuesObject) { private static List<KvEntry> parseValues(JsonObject valuesObject) {

10
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -522,7 +522,7 @@ public class ProtoUtils {
} }
private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) { private static TransportProtos.ToDeviceRpcRequestActorMsgProto toProto(ToDeviceRpcRequestActorMsg msg) {
TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder() TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder()
.setMethodName(msg.getMsg().getBody().getMethod()) .setMethodName(msg.getMsg().getBody().getMethod())
.setParams(msg.getMsg().getBody().getParams()) .setParams(msg.getMsg().getBody().getParams())
.setExpirationTime(msg.getMsg().getExpirationTime()) .setExpirationTime(msg.getMsg().getExpirationTime())
@ -530,7 +530,11 @@ public class ProtoUtils {
.setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits()) .setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits())
.setOneway(msg.getMsg().isOneway()) .setOneway(msg.getMsg().isOneway())
.setPersisted(msg.getMsg().isPersisted()) .setPersisted(msg.getMsg().isPersisted())
.build(); .setAdditionalInfo(msg.getMsg().getAdditionalInfo());
if (msg.getMsg().getRetries() != null) {
builder.setRetries(msg.getMsg().getRetries());
}
TransportProtos.ToDeviceRpcRequestMsg proto = builder.build();
return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder() return TransportProtos.ToDeviceRpcRequestActorMsgProto.newBuilder()
.setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits()) .setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits())
@ -551,7 +555,7 @@ public class ProtoUtils {
toDeviceRpcRequestMsg.getOneway(), toDeviceRpcRequestMsg.getOneway(),
toDeviceRpcRequestMsg.getExpirationTime(), toDeviceRpcRequestMsg.getExpirationTime(),
new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()), new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()),
toDeviceRpcRequestMsg.getPersisted(), 0, ""); toDeviceRpcRequestMsg.getPersisted(), toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, toDeviceRpcRequestMsg.getAdditionalInfo());
return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest); return new ToDeviceRpcRequestActorMsg(proto.getServiceId(), toDeviceRpcRequest);
} }

4
common/proto/src/main/proto/queue.proto

@ -64,7 +64,7 @@ enum EntityTypeProto {
CALCULATED_FIELD = 39; CALCULATED_FIELD = 39;
CALCULATED_FIELD_LINK = 40; CALCULATED_FIELD_LINK = 40;
JOB = 41; JOB = 41;
AI_MODEL_SETTINGS = 42; AI_MODEL = 42;
} }
enum ApiUsageRecordKeyProto { enum ApiUsageRecordKeyProto {
@ -697,6 +697,8 @@ message ToDeviceRpcRequestMsg {
int64 requestIdLSB = 6; int64 requestIdLSB = 6;
bool oneway = 7; bool oneway = 7;
bool persisted = 8; bool persisted = 8;
optional int32 retries = 9;
string additionalInfo = 10;
} }
message ToDeviceRpcResponseMsg { message ToDeviceRpcResponseMsg {

5
common/proto/src/test/java/org/thingsboard/server/common/adaptor/JsonConverterTest.java

@ -23,8 +23,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.api.parallel.Isolated;
import java.util.ArrayList;
@Isolated("JsonConverter static settings being modified") @Isolated("JsonConverter static settings being modified")
public class JsonConverterTest { public class JsonConverterTest {
@ -53,7 +51,7 @@ public class JsonConverterTest {
@Test @Test
public void testParseAttributesBigDecimalAsLong() { public void testParseAttributesBigDecimalAsLong() {
var result = new ArrayList<>(JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"))); var result = JsonConverter.convertToAttributes(JsonParser.parseString("{\"meterReadingDelta\": 1E1}"));
Assertions.assertEquals(10L, result.get(0).getLongValue().get().longValue()); Assertions.assertEquals(10L, result.get(0).getLongValue().get().longValue());
} }
@ -108,4 +106,5 @@ public class JsonConverterTest {
JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); JsonConverter.convertToTelemetry(JsonParser.parseString("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L);
}); });
} }
} }

27
common/queue/src/main/java/org/thingsboard/server/queue/common/PartitionedQueueResponseTemplate.java

@ -21,9 +21,11 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueHandler;
import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
@ -119,8 +121,20 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
response -> { response -> {
pendingRequestCount.decrementAndGet(); pendingRequestCount.decrementAndGet();
response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
responseProducer.send(TopicPartitionInfo.builder().topic(responseTopic).build(), response, null); TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(responseTopic).build();
stats.incrementSuccessful(); responseProducer.send(tpi, response, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
stats.incrementSuccessful();
}
@Override
public void onFailure(Throwable t) {
log.error("[{}] Failed to send response {}", requestId, response, t);
sendErrorResponse(requestId, tpi, request, t);
stats.incrementFailed();
}
});
}, },
e -> { e -> {
pendingRequestCount.decrementAndGet(); pendingRequestCount.decrementAndGet();
@ -144,6 +158,15 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
consumer.commit(); consumer.commit();
} }
private void sendErrorResponse(UUID requestId, TopicPartitionInfo tpi, Request request, Throwable cause) {
Response errorResponseMsg = handler.constructErrorResponseMsg(request, cause);
if (errorResponseMsg != null) {
errorResponseMsg.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
responseProducer.send(tpi, errorResponseMsg, null);
}
}
public void subscribe(Set<TopicPartitionInfo> partitions) { public void subscribe(Set<TopicPartitionInfo> partitions) {
requestConsumer.update(partitions); requestConsumer.update(partitions);
} }

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

Loading…
Cancel
Save