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.
--
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START
-- UPDATE OTA PACKAGE EXTERNAL ID START
UPDATE tenant_profile
SET profile_data = jsonb_set(
profile_data,
'{configuration}',
(
(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';
ALTER TABLE ota_package
ADD COLUMN IF NOT EXISTS external_id uuid;
ALTER TABLE ota_package
ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
-- 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
SET trigger_config = REGEXP_REPLACE(
trigger_config,
'"CASSANDRA_QUERIES"',
'"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"',
'g'
)
WHERE trigger_type = 'RATE_LIMITS'
AND trigger_config LIKE '%"CASSANDRA_QUERIES"%';
DROP INDEX IF EXISTS idx_device_external_id;
DROP INDEX IF EXISTS idx_device_profile_external_id;
DROP INDEX IF EXISTS idx_asset_external_id;
DROP INDEX IF EXISTS idx_entity_view_external_id;
DROP INDEX IF EXISTS idx_rule_chain_external_id;
DROP INDEX IF EXISTS idx_dashboard_external_id;
DROP INDEX IF EXISTS idx_customer_external_id;
DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
-- 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.MqttClientSettings;
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.notification.SlackService;
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.tools.TbRateLimits;
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.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -315,11 +315,11 @@ public class ActorSystemContext {
@Autowired
@Getter
private RuleEngineAiModelService aiModelService;
private RuleEngineAiChatModelService aiChatModelService;
@Autowired
@Getter
private AiModelSettingsService aiModelSettingsService;
private AiModelService aiModelService;
@Autowired
@Getter

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

@ -68,7 +68,7 @@ import java.util.stream.Collectors;
*/
@Slf4j
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;
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.MqttClientSettings;
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.RuleEngineApiUsageStateService;
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.queue.ServiceType;
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.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -1027,13 +1027,13 @@ public class DefaultTbContext implements TbContext {
}
@Override
public RuleEngineAiModelService getAiModelService() {
return mainCtx.getAiModelService();
public RuleEngineAiChatModelService getAiChatModelService() {
return mainCtx.getAiChatModelService();
}
@Override
public AiModelSettingsService getAiModelSettingsService() {
return mainCtx.getAiModelSettingsService();
public AiModelService getAiModelService() {
return mainCtx.getAiModelService();
}
@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 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 lombok.RequiredArgsConstructor;
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.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.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.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.util.Optional;
import java.util.UUID;
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;
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/ai/model")
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(
value = "Send request to AI chat model (sendChatRequest)",
@ -53,13 +163,13 @@ class AiModelController extends BaseController {
@PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
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())
.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);
}

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.TenantProfile;
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.AlarmComment;
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.ThingsboardErrorCode;
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.AlarmId;
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.WidgetTypeInfo;
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.asset.AssetProfileService;
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.component.ComponentDiscoveryService;
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.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -383,10 +383,10 @@ public abstract class BaseController {
protected CalculatedFieldService calculatedFieldService;
@Autowired
protected AiModelSettingsService aiModelSettingsService;
protected AiModelService aiModelService;
@Autowired
protected TbAiModelSettingsService tbAiModelSettingsService;
protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}")
@Getter
@ -400,7 +400,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e);
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();
} else {
e = thingsboardException;
@ -448,7 +448,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} 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);
} else if (exception instanceof MessagingException) {
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_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(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);
};
} catch (Exception e) {
@ -848,8 +848,8 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation);
}
AiModelSettings checkAiModelSettingsId(AiModelSettingsId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelSettingsService.findAiModelSettingsByTenantIdAndId(tenantId, id).orElse(null), operation);
AiModel checkAiModelId(AiModelId settingsId, Operation operation) throws ThingsboardException {
return checkEntityId(settingsId, (tenantId, id) -> aiModelService.findAiModelByTenantIdAndId(tenantId, id).orElse(null), operation);
}
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 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 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 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.ResponseEntity;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
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.Resource;
import java.io.IOException;
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.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)
@PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}/download")
public ResponseEntity<org.springframework.core.io.Resource> downloadOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
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. " +
OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/info/{otaPackageId}")
public OtaPackageInfo getOtaPackageInfoById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
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. " +
"The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackage/{otaPackageId}")
public OtaPackage getOtaPackageById(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException {
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. " +
"\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage", method = RequestMethod.POST)
@ResponseBody
@PostMapping(value = "/otaPackage")
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());
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,
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(mediaType = MULTIPART_FORM_DATA_VALUE)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST, consumes = MULTIPART_FORM_DATA_VALUE)
@ResponseBody
@PostMapping(value = "/otaPackage/{otaPackageId}", consumes = MULTIPART_FORM_DATA_VALUE)
public OtaPackageInfo saveOtaPackageData(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId,
@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"}))
@RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr,
@Parameter(description = "OTA Package data.")
@RequestPart MultipartFile file) throws ThingsboardException, IOException {
@RequestPart MultipartFile file) throws Exception {
checkParameter(OTA_PACKAGE_ID, strOtaPackageId);
checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr);
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. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@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. " +
PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET)
@ResponseBody
@GetMapping(value = "/otaPackages/{deviceProfileId}/{type}")
public PageData<OtaPackageInfo> getOtaPackages(@Parameter(description = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceProfileId") String strDeviceProfileId,
@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. " +
"Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE)
@ResponseBody
@DeleteMapping(value = "/otaPackage/{otaPackageId}")
public void deleteOtaPackage(@Parameter(description = OTA_PACKAGE_ID_PARAM_DESCRIPTION)
@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException {
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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
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.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.kv.Aggregation;
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.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
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.JsonDataEntry;
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.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.rule.engine.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.config.annotations.ApiOperation;
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.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -156,9 +148,6 @@ public class TelemetryController extends BaseController {
@Autowired
private TbTelemetryService tbTelemetryService;
@Value("${transport.json.max_string_value_length:0}")
private int maxStringValueLength;
private ExecutorService executor;
@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.")
@RequestParam(name = "timeZone", required = false) String timeZone,
@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,
@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"}))
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@ -337,20 +326,21 @@ public class TelemetryController extends BaseController {
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = SAVE_ATTIRIBUTES_STATUS_OK +
"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'."),
"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'."),
@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 = "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')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveDeviceAttributes(
@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)) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{deviceId}/{scope}")
public DeferredResult<ResponseEntity> saveDeviceAttributes(@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))
@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.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
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),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV1(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{entityType}/{entityId}/{scope}")
public DeferredResult<ResponseEntity> saveEntityAttributesV1(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@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);
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),
})
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV2(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
@PostMapping(value = "/{entityType}/{entityId}/attributes/{scope}")
public DeferredResult<ResponseEntity> saveEntityAttributesV2(@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@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);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ -460,11 +454,11 @@ public class TelemetryController extends BaseController {
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@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 = "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. " +
"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')")
@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)
@ApiResponses(value = {
@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 = "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. " +
"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')")
@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)
@ApiResponses(value = {
@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 = "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. " +
"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')")
@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) {
return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
}
if (json.isObject()) {
List<AttributeKvEntry> attributes = extractRequestAttributes(json);
JsonElement 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()) {
return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
}
for (AttributeKvEntry attributeKvEntry : attributes) {
if (attributeKvEntry.getKey().isEmpty() || attributeKvEntry.getKey().trim().length() == 0) {
return getImmediateDeferredResult("Key cannot be empty or contains only spaces", HttpStatus.BAD_REQUEST);
if (attributeKvEntry.getKey().isBlank()) {
return getImmediateDeferredResult("Key cannot be blank", HttpStatus.BAD_REQUEST);
}
}
SecurityUser user = getCurrentUser();
@ -885,43 +885,6 @@ public class TelemetryController extends BaseController {
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) {
try {
return JacksonUtil.toJsonNode(value);

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

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

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;
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 lombok.RequiredArgsConstructor;
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.Langchain4jChatModelConfigurer;
@Service
@RequiredArgsConstructor
class AiModelServiceImpl implements AiModelService {
class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor;
@Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModel<C> chatModel, ChatRequest chatRequest) {
ChatModel lc4jChatModel = chatModel.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(lc4jChatModel, chatRequest);
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
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.PredictionServiceSettings;
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.chat.ChatModel;
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 org.springframework.stereotype.Component;
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.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.Langchain4jChatModelConfigurer;
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.model.chat.MistralAiChatModelConfig;
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.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
@ -54,61 +60,57 @@ import java.time.Duration;
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override
public ChatModel configureChatModel(OpenAiChatModel chatModel) {
OpenAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.openai.OpenAiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
return OpenAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AzureOpenAiChatModel chatModel) {
AzureOpenAiProviderConfig providerConfig = chatModel.providerConfig();
AzureOpenAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.azure.AzureOpenAiChatModel.builder()
public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
return AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey())
.deploymentName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
.deploymentName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleAiGeminiChatModel chatModel) {
GoogleAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.googleai.GoogleAiGeminiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.topK(modelConfig.topK())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxOutputTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(GoogleAiGeminiChatModelConfig chatModelConfig) {
return GoogleAiGeminiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModel chatModel) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModel.providerConfig();
GoogleVertexAiGeminiChatModel.Config modelConfig = chatModel.modelConfig();
public ChatModel configureChatModel(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
// construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials;
@ -131,8 +133,8 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.toBuilder();
// set request timeout from model config
if (modelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(modelConfig.timeoutSeconds()));
if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
@ -154,30 +156,30 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder();
if (modelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(modelConfig.temperature().floatValue());
if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
}
if (modelConfig.topP() != null) {
generationConfigBuilder.setTopP(modelConfig.topP().floatValue());
if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
}
if (modelConfig.topK() != null) {
generationConfigBuilder.setTopK(modelConfig.topK());
if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(chatModelConfig.topK());
}
if (modelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(modelConfig.frequencyPenalty().floatValue());
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (modelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(modelConfig.frequencyPenalty().floatValue());
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (modelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(modelConfig.maxOutputTokens());
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
}
var generationConfig = generationConfigBuilder.build();
// 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) {
@ -189,40 +191,37 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
}
@Override
public ChatModel configureChatModel(MistralAiChatModel chatModel) {
MistralAiChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.mistralai.MistralAiChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(MistralAiChatModelConfig chatModelConfig) {
return MistralAiChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AnthropicChatModel chatModel) {
AnthropicChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.anthropic.AnthropicChatModel.builder()
.apiKey(chatModel.providerConfig().apiKey())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.topK(modelConfig.topK())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(AnthropicChatModelConfig chatModelConfig) {
return AnthropicChatModel.builder()
.apiKey(chatModelConfig.providerConfig().apiKey())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(AmazonBedrockChatModel chatModel) {
AmazonBedrockProviderConfig providerConfig = chatModel.providerConfig();
AmazonBedrockChatModel.Config modelConfig = chatModel.modelConfig();
public ChatModel configureChatModel(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
@ -234,33 +233,32 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build();
var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.maxOutputTokens(modelConfig.maxOutputTokens())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.build();
return BedrockChatModel.builder()
.client(bedrockClient)
.modelId(modelConfig.modelId())
.modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
public ChatModel configureChatModel(GitHubModelsChatModel chatModel) {
GitHubModelsChatModel.Config modelConfig = chatModel.modelConfig();
return dev.langchain4j.model.github.GitHubModelsChatModel.builder()
.gitHubToken(chatModel.providerConfig().personalAccessToken())
.modelName(modelConfig.modelId())
.temperature(modelConfig.temperature())
.topP(modelConfig.topP())
.frequencyPenalty(modelConfig.frequencyPenalty())
.presencePenalty(modelConfig.presencePenalty())
.maxTokens(modelConfig.maxOutputTokens())
.timeout(toDuration(modelConfig.timeoutSeconds()))
.maxRetries(modelConfig.maxRetries())
public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig) {
return GitHubModelsChatModel.builder()
.gitHubToken(chatModelConfig.providerConfig().personalAccessToken())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.frequencyPenalty(chatModelConfig.frequencyPenalty())
.presencePenalty(chatModelConfig.presencePenalty())
.maxTokens(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.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.AssetService;
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.dashboard.DashboardService;
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.asset.AssetEdgeProcessor;
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.device.DeviceEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.device.profile.DeviceProfileEdgeProcessor;
@ -248,6 +250,12 @@ public class EdgeContextComponent {
@Autowired
private GrpcCallbackExecutorService grpcCallbackExecutorService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private CalculatedFieldProcessor calculatedFieldProcessor;
public EdgeProcessor getProcessor(EdgeEventType edgeEventType) {
EdgeProcessor processor = processorMap.get(edgeEventType);
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.EntityAlarm;
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.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -112,7 +113,7 @@ public class EdgeEventSourcingListener {
return;
}
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;
}
log.trace("[{}] DeleteEntityEvent called: {}", tenantId, event);
@ -226,7 +227,7 @@ public class EdgeEventSourcingListener {
break;
case TENANT:
return !event.getCreated();
case API_USAGE_STATE, EDGE, AI_MODEL_SETTINGS:
case API_USAGE_STATE, EDGE, AI_MODEL:
return false;
case DOMAIN:
if (entity instanceof Domain domain) {
@ -262,6 +263,8 @@ public class EdgeEventSourcingListener {
private String getBodyMsgForEntityEvent(Object entity) {
if (entity instanceof AlarmComment) {
return JacksonUtil.toString(entity);
} else if (entity instanceof CalculatedField calculatedField) {
return JacksonUtil.toString(calculatedField.getEntityId());
}
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.asset.Asset;
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.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
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.DashboardId;
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.AssetUpdateMsg;
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.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
@ -638,4 +641,17 @@ public class EdgeMsgConstructorUtils {
.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(
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) {
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
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, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
@ -283,9 +285,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
session.destroy();
destroySession(session);
session.cleanUp();
session.close();
sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
@ -521,7 +522,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void destroySession(EdgeGrpcSession 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) {
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) {
kafkaSession.destroy();
if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
}
}
} 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.AssetUpdateMsg;
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.ConnectResponseCode;
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());
if (attempt > 1) {
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) {
// Send a failure notification only on the second attempt.
// This ensures that failure alerts are sent just once to avoid redundant notifications.
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.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());
for (DownlinkMsg downlinkMsg : copy) {
@ -882,6 +885,11 @@ public abstract class EdgeGrpcSession implements Closeable {
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) {
for (UserCredentialsRequestMsg userCredentialsRequestMsg : uplinkMsg.getUserCredentialsRequestMsgList()) {
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));
}
}
if (uplinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : uplinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg));
}
}
} catch (Exception e) {
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);
@ -917,7 +930,9 @@ public abstract class EdgeGrpcSession implements Closeable {
return Futures.allAsList(result);
}
protected void destroy() {}
protected boolean destroy() {
return true;
}
protected void cleanUp() {}

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

@ -107,4 +107,5 @@ public class EdgeSyncCursor {
currentIdx++;
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
public void destroy() {
public boolean destroy() {
try {
if (consumer != null) {
consumer.stop();
}
} finally {
consumer = null;
} catch (Exception e) {
log.warn("[{}][{}] Failed to stop edge event consumer", tenantId, edge.getId(), e);
return false;
}
consumer = null;
try {
if (consumerExecutor != null) {
consumerExecutor.shutdown();
}
} catch (Exception ignored) {}
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
}
@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;
default -> switch (type) {
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,
NOTIFICATION_RULE -> true;
WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, CALCULATED_FIELD, NOTIFICATION_TEMPLATE,
NOTIFICATION_TARGET, NOTIFICATION_RULE -> true;
default -> false;
};
};
@ -222,7 +222,7 @@ public abstract class BaseEdgeProcessor implements EdgeProcessor {
if (edgeId != null && !edgeId.equals(originatorEdgeId)) {
return saveEdgeEvent(tenantId, edgeId, type, actionType, entityId, body);
} else {
return processNotificationToRelatedEdges(tenantId, entityId, type, actionType, originatorEdgeId);
return processNotificationToRelatedEdges(tenantId, entityId, entityId, type, actionType, originatorEdgeId);
}
case 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,
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
protected ListenableFuture<Void> processNotificationToRelatedEdges(TenantId tenantId, EntityId ownerEntityId, EntityId entityId, EdgeEventType type,
EdgeEventActionType actionType, EdgeId sourceEdgeId) {
List<ListenableFuture<Void>> futures = new ArrayList<>();
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) {
if (!relatedEdgeId.equals(sourceEdgeId)) {
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()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAssetUpdateMsg(assetUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = edgeCtx.getAssetProfileService().findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
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);
builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build();
}
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = edgeCtx.getDeviceProfileService().findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
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();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
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);
Futures.addCallback(future, new FutureCallback<>() {
@Override
@ -314,7 +314,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
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);
Futures.addCallback(future, new FutureCallback<>() {
@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.WidgetsBundle;
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.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
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.EntityViewsRequestMsg;
import org.thingsboard.server.gen.edge.v1.RelationRequestMsg;
@ -90,7 +92,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private TimeseriesService timeseriesService;
@Autowired
private RelationService relationService;
@ -104,6 +106,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private DbCallbackExecutorService dbCallbackExecutorService;
@ -293,6 +298,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
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) {
EntityRelationsQuery query = new EntityRelationsQuery();
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.id.TenantId;
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.EntityViewsRequestMsg;
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> processCalculatedFieldRequestMsg(TenantId tenantId, Edge edge, CalculatedFieldRequestMsg calculatedFieldRequestMsg);
@Deprecated(since = "3.9.1", forRemoval = true)
ListenableFuture<Void> processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg);
@ -46,4 +49,5 @@ public interface EdgeRequestsService {
@Deprecated(since = "3.9.1", forRemoval = true)
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.thingsboard.server.common.data.EntityType;
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.dao.ai.AiModelSettingsService;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -30,48 +30,48 @@ import static java.util.Objects.requireNonNullElseGet;
@Service
@TbCoreComponent
@RequiredArgsConstructor
class DefaultTbAiModelSettingsService extends AbstractTbEntityService implements TbAiModelSettingsService {
class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelSettingsService aiModelSettingsService;
private final AiModelService aiModelService;
@Override
public AiModelSettings save(AiModelSettings settings, User user) {
var actionType = settings.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
public AiModel save(AiModel model, User user) {
var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId();
settings.setTenantId(tenantId);
model.setTenantId(tenantId);
AiModelSettings savedSettings;
AiModel savedModel;
try {
savedSettings = aiModelSettingsService.save(settings);
autoCommit(user, savedSettings.getId());
savedModel = aiModelService.save(model);
autoCommit(user, savedModel.getId());
} 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;
}
logEntityActionService.logEntityAction(tenantId, savedSettings.getId(), savedSettings, actionType, user);
logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedSettings;
return savedModel;
}
@Override
public boolean delete(AiModelSettings settings, User user) {
public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED;
var tenantId = user.getTenantId();
var settingsId = settings.getId();
var modelId = model.getId();
boolean deleted;
try {
deleted = aiModelSettingsService.deleteByTenantIdAndId(tenantId, settingsId);
deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} 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;
}
if (deleted) {
logEntityActionService.logEntityAction(tenantId, settingsId, settings, actionType, user, settingsId.toString());
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
}
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;
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;
}
}
}

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.
// 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 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.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
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.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue;
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.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
@ -46,108 +38,27 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE;
@Service
@Profile("install")
@Slf4j
@RequiredArgsConstructor
public class DefaultDataUpdateService implements DataUpdateService {
private static final int MAX_PENDING_SAVE_RULE_NODE_FUTURES = 256;
private static final int DEFAULT_PAGE_SIZE = 1024;
@Autowired
private RuleChainService ruleChainService;
@Autowired
private RelationService relationService;
@Autowired
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private DbUpgradeExecutorService executorService;
@Autowired
private TenantProfileService tenantProfileService;
private final RuleChainService ruleChainService;
private final ComponentDiscoveryService componentDiscoveryService;
private final DbUpgradeExecutorService executorService;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
//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;
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);
log.info("Data updated.");
}
@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),
MOBILE_APP_SETTINGS,
JOB(EntityType.JOB),
AI_MODEL_SETTINGS(EntityType.AI_MODEL_SETTINGS);
AI_MODEL(EntityType.AI_MODEL);
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.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
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_BUNDLE, tenantEntityPermissionChecker);
put(Resource.JOB, tenantEntityPermissionChecker);
put(Resource.AI_MODEL_SETTINGS, aiModelSettingsPermissionChecker);
put(Resource.AI_MODEL, aiModelPermissionChecker);
}
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
public boolean hasPermission(SecurityUser user, Operation operation) {
@ -157,7 +157,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
}
@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());
}

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(
EntityType.CUSTOMER, EntityType.RULE_CHAIN, EntityType.TB_RESOURCE,
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.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE,
EntityType.AI_MODEL_SETTINGS
EntityType.AI_MODEL
);
@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.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -26,11 +26,11 @@ import java.util.Set;
@Service
@TbCoreComponent
class AiModelSettingsExportService extends BaseEntityExportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>> {
class AiModelExportService extends BaseEntityExportService<AiModelId, AiModel, EntityExportData<AiModel>> {
@Override
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) {
device.setCustomerId(getExternalIdOrElseInternal(ctx, device.getCustomerId()));
device.setDeviceProfileId(getExternalIdOrElseInternal(ctx, device.getDeviceProfileId()));
device.setFirmwareId(getExternalIdOrElseInternal(ctx, device.getFirmwareId()));
device.setSoftwareId(getExternalIdOrElseInternal(ctx, device.getSoftwareId()));
if (ctx.getSettings().isExportCredentials()) {
var credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(ctx.getTenantId(), device.getId());
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.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultRuleChainId()));
deviceProfile.setDefaultEdgeRuleChainId(getExternalIdOrElseInternal(ctx, deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setFirmwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getFirmwareId()));
deviceProfile.setSoftwareId(getExternalIdOrElseInternal(ctx, deviceProfile.getSoftwareId()));
}
@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.utils.CsvUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
@ -235,7 +234,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
@SneakyThrows
private void saveAttributes(SecurityUser user, E entity, Map.Entry<BulkImportColumnType, JsonObject> kvsEntry, BulkImportColumnType kvType) {
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) -> {
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 org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.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.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@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
protected void setOwner(
TenantId tenantId,
AiModelSettings settings,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider
AiModel model,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
settings.setTenantId(tenantId);
model.setTenantId(tenantId);
}
@Override
protected AiModelSettings prepare(
protected AiModel prepare(
EntitiesImportCtx ctx,
AiModelSettings settings,
AiModelSettings oldEntity,
EntityExportData<AiModelSettings> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider
AiModel model,
AiModel oldModel,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
return settings;
return model;
}
@Override
protected AiModelSettings deepCopy(AiModelSettings settings) {
return new AiModelSettings(settings);
protected AiModel deepCopy(AiModel model) {
return new AiModel(model);
}
@Override
protected AiModelSettings saveOrUpdate(
protected AiModel saveOrUpdate(
EntitiesImportCtx ctx,
AiModelSettings settings,
EntityExportData<AiModelSettings> exportData,
BaseEntityImportService<AiModelSettingsId, AiModelSettings, EntityExportData<AiModelSettings>>.IdProvider idProvider,
AiModel model,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider,
CompareResult compareResult
) {
return aiModelSettingsService.save(settings);
return aiModelService.save(model);
}
@Override
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.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -148,6 +147,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
public CompareResult(boolean updateNeeded) {
this.updateNeeded = updateNeeded;
}
}
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 void processAfterSaved(EntitiesImportCtx ctx, EntityImportResult<E> importResult, D exportData, IdProvider idProvider) throws ThingsboardException {
E savedEntity = importResult.getSavedEntity();
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) {
if (externalId == null || externalId.isNullUid()) return null;
if (externalId == null || externalId.isNullUid()) {
return null;
}
if (EntityType.TENANT.equals(externalId.getEntityType())) {
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) {
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
if (externalUuid.equals(EntityId.NULL_UUID)) {
return Optional.empty();
}
for (EntityType entityType : EntityType.values()) {
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,
Set<String> skippedRootFields, Pattern includedFieldsPattern,
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
protected Device prepare(EntitiesImportCtx ctx, Device device, Device old, DeviceExportData exportData, IdProvider idProvider) {
device.setDeviceProfileId(idProvider.getInternalId(device.getDeviceProfileId()));
device.setFirmwareId(getOldEntityField(old, Device::getFirmwareId));
device.setSoftwareId(getOldEntityField(old, Device::getSoftwareId));
device.setFirmwareId(idProvider.getInternalId(device.getFirmwareId()));
device.setSoftwareId(idProvider.getInternalId(device.getSoftwareId()));
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.setDefaultEdgeRuleChainId(idProvider.getInternalId(deviceProfile.getDefaultEdgeRuleChainId()));
deviceProfile.setDefaultDashboardId(idProvider.getInternalId(deviceProfile.getDefaultDashboardId()));
deviceProfile.setFirmwareId(getOldEntityField(old, DeviceProfile::getFirmwareId));
deviceProfile.setSoftwareId(getOldEntityField(old, DeviceProfile::getSoftwareId));
deviceProfile.setFirmwareId(idProvider.getInternalId(deviceProfile.getFirmwareId(), false));
deviceProfile.setSoftwareId(idProvider.getInternalId(deviceProfile.getSoftwareId(), false));
return deviceProfile;
}
@Override
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);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
if (toUpdate) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
@ -73,8 +78,6 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override
protected void cleanupForComparison(DeviceProfile deviceProfile) {
super.cleanupForComparison(deviceProfile);
deviceProfile.setFirmwareId(null);
deviceProfile.setSoftwareId(null);
}
@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}"
# Interval in milliseconds for printing Cassandra query queue statistic
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}"
# log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
@ -656,9 +656,9 @@ cache:
trendzSettings:
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
aiModelSettings:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_SETTINGS_TTL:1440}" # AI model settings cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
aiModel:
timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL
maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled
# Deliberately placed outside the 'specs' group above
notificationRules:
@ -874,7 +874,7 @@ audit-log:
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource 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.
"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:
# Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}"
@ -1673,7 +1673,7 @@ queue:
# 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}"
# 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
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

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());
}
@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
public void testDeleteTbResource() throws Exception {
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 {
return doGet("/api/resource/" + resourceId + "/download")
.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.RuleChainMetaData;
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.controller.AbstractControllerTest;
import org.thingsboard.server.dao.edge.EdgeEventService;
@ -565,7 +566,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
protected Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception {
// create device and assign to edge
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()
+ "/device/" + savedDevice.getUuidId(), Device.class);
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(thermostatDeviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB());
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;
}

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.AssetProfileUpdateMsg;
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.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
@ -352,6 +353,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(notificationTargetUpdateMsg));
}
}
if (downlinkMsg.getCalculatedFieldUpdateMsgCount() > 0) {
for (CalculatedFieldUpdateMsg calculatedFieldUpdateMsg : downlinkMsg.getCalculatedFieldUpdateMsgList()) {
result.add(saveDownlinkMsg(calculatedFieldUpdateMsg));
}
}
if (downlinkMsg.hasEdgeConfiguration()) {
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 {
msgStr = "{}";
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{
outsidePolygon: isInsidePolygon(37.8000, -122.4300, perimeter),
insidePolygon: isInsidePolygon(37.7725, -122.4010, perimeter),
@ -2470,7 +2470,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
public void isInsideCircle_Test() throws ExecutionException, InterruptedException {
msgStr = "{}";
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{
outsideCircle: isInsideCircle(37.8044, -122.2712, 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.EntityId;
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.TenantId;
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");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 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());
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 -> {
try {
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(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();
verify(entityActionService).logEntityAction(any(), eq(importedDevice.getId()), eq(importedDevice),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).onDeviceUpdated(eq(importedDevice), isNull());
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
assertThat(importedDevice.getFirmwareId()).isEqualTo(importedFirmware.getId());
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
@ -318,14 +325,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assetProfile = assetProfileService.saveAssetProfile(assetProfile);
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");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>();
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);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false)
@ -359,12 +367,17 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.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();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.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();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
@ -380,13 +393,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
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.setTenantId(tenantId);
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);
device.setFirmwareId(firmwareId);
device.setSoftwareId(softwareId);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
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.awaitility.Awaitility.await;
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.TEST_DATA;
@DaoSqlTest
public class VersionControlTest extends AbstractControllerTest {
@ -262,19 +262,24 @@ public class VersionControlTest extends AbstractControllerTest {
}
@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");
createVersion("profiles", EntityType.DEVICE_PROFILE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1");
String versionId = createVersion("devices", EntityType.DEVICE);
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
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 newCredentials = new DeviceCredentials(deviceCredentials);
newCredentials.setCredentialsId("new access token"); // updating access token to avoid constraint errors on import
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();
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_PROFILE).getCreated()).isEqualTo(1);
@ -293,6 +298,13 @@ public class VersionControlTest extends AbstractControllerTest {
assertThat(importedCredentials.getCredentialsId()).isEqualTo(deviceCredentials.getCredentialsId());
assertThat(importedCredentials.getCredentialsValue()).isEqualTo(deviceCredentials.getCredentialsValue());
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
@ -653,6 +665,57 @@ public class VersionControlTest extends AbstractControllerTest {
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
public void testResourceVc_sameTenant() throws Exception {
TbResourceInfo resourceInfo = createResource("Test resource");
@ -923,6 +986,7 @@ public class VersionControlTest extends AbstractControllerTest {
otaPackage.setDeviceProfileId(deviceProfileId);
otaPackage.setType(type);
otaPackage.setTitle("My " + type);
otaPackage.setTag("My " + type);
otaPackage.setVersion("v1.0");
otaPackage.setFileName("filename.txt");
otaPackage.setContentType("text/plain");
@ -933,6 +997,10 @@ public class VersionControlTest extends AbstractControllerTest {
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) {
Dashboard dashboard = new Dashboard();
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.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import java.io.Serial;
import java.io.Serializable;
@ -34,12 +33,11 @@ public class ResourceInfoCacheKey implements Serializable {
@Serial
private static final long serialVersionUID = 2100510964692846992L;
private final TenantId tenantId;
private final TbResourceId tbResourceId;
@Override
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;
/**
* Created by ashvayka on 05.10.18.
*/
public interface TbQueueHandler<Request extends TbQueueMsg, Response extends TbQueueMsg> {
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;
import com.google.common.util.concurrent.FluentFuture;
import org.thingsboard.server.common.data.ai.AiModelSettings;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -25,18 +25,18 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
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, boolean doValidate);
CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
CalculatedField findByEntityIdAndName(EntityId entityId, String name);
List<CalculatedFieldId> findCalculatedFieldIdsByEntityId(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);
OtaPackage findOtaPackageByTenantIdAndTitleAndVersion(TenantId tenantId, String title, String version);
ListenableFuture<OtaPackageInfo> findOtaPackageInfoByIdAsync(TenantId tenantId, OtaPackageId otaPackageId);
PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink);
@ -52,4 +54,5 @@ public interface OtaPackageService extends EntityDaoService {
void deleteOtaPackagesByTenantId(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 {
private CacheConstants() {}
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations";
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 SENT_NOTIFICATIONS_CACHE = "sentNotifications";
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 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_LINK(40),
JOB(41),
AI_MODEL_SETTINGS(42, "ai_model_settings") {
AI_MODEL(42, "ai_model") {
@Override
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 org.thingsboard.server.common.data.id.OtaPackageId;
import java.io.Serial;
import java.nio.ByteBuffer;
@Schema
@ -27,6 +28,7 @@ import java.nio.ByteBuffer;
@EqualsAndHashCode(callSuper = true)
public class OtaPackage extends OtaPackageInfo {
@Serial
private static final long serialVersionUID = 3091601761339422546L;
@Schema(description = "OTA Package data.", accessMode = Schema.AccessMode.READ_ONLY)
@ -44,4 +46,10 @@ public class OtaPackage extends OtaPackageInfo {
super(otaPackage);
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;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
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.NoXss;
import java.io.Serial;
@Schema
@Slf4j
@Data
@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;
@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)
private Long dataSize;
private OtaPackageId externalId;
public OtaPackageInfo() {
super();
}
@ -100,6 +106,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
this.checksumAlgorithm = otaPackageInfo.getChecksumAlgorithm();
this.checksum = otaPackageInfo.getChecksum();
this.dataSize = otaPackageInfo.getDataSize();
this.externalId = otaPackageInfo.getExternalId();
}
@Schema(description = "JSON object with the ota package Id. " +
@ -118,7 +125,7 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
}
@Override
@JsonIgnore
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getName() {
return title;
}
@ -133,4 +140,5 @@ public class OtaPackageInfo extends BaseDataWithAdditionalInfo<OtaPackageId> imp
public JsonNode 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 org.thingsboard.server.common.data.id.TbResourceId;
import java.io.Serial;
import java.util.Base64;
import java.util.Optional;
@ -31,6 +32,7 @@ import java.util.Optional;
@EqualsAndHashCode(callSuper = true)
public class TbResource extends TbResourceInfo {
@Serial
private static final long serialVersionUID = 7379609705527272306L;
private byte[] data;
@ -88,4 +90,5 @@ public class TbResource extends TbResourceInfo {
public String 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.HasTenantId;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ai.model.AiModel;
import org.thingsboard.server.common.data.id.AiModelSettingsId;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoNullChar;
@ -39,7 +39,7 @@ import java.io.Serial;
@Builder
@AllArgsConstructor
@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
private static final long serialVersionUID = 9017108678716011604L;
@ -47,7 +47,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
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"
)
private TenantId tenantId;
@ -55,7 +55,7 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
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",
defaultValue = "1"
)
@ -67,8 +67,8 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Human-readable name of the AI model settings; must be unique within the scope of the tenant",
example = "Rule node assistant"
description = "Display name for this AI model configuration; not the technical model identifier",
example = "Fast and cost-efficient model"
)
private String name;
@ -79,24 +79,24 @@ public final class AiModelSettings extends BaseData<AiModelSettingsId> implement
accessMode = Schema.AccessMode.READ_WRITE,
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);
}
public AiModelSettings(AiModelSettings settings) {
super(settings.getId());
createdTime = settings.getCreatedTime();
tenantId = settings.getTenantId();
version = settings.getVersion();
name = settings.getName();
configuration = settings.getConfiguration();
externalId = settings.getExternalId() == null ? null : new AiModelSettingsId(settings.getExternalId().getId());
public AiModel(AiModel model) {
super(model.getId());
createdTime = model.getCreatedTime();
tenantId = model.getTenantId();
version = model.getVersion();
name = model.getName();
configuration = model.getConfiguration();
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 jakarta.validation.Valid;
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.List;
@ -51,7 +51,7 @@ public record TbChatRequest(
description = "Configuration of the AI chat model that should execute the request"
)
@NotNull @Valid
AiChatModel<?> chatModel
AiChatModelConfig<?> chatModelConfig
) {
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;
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 {
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;
import dev.langchain4j.model.chat.ChatModel;
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
permits
OpenAiChatModel.Config, AzureOpenAiChatModel.Config, GoogleAiGeminiChatModel.Config,
GoogleVertexAiGeminiChatModel.Config, MistralAiChatModel.Config, AnthropicChatModel.Config,
AmazonBedrockChatModel.Config, GitHubModelsChatModel.Config {
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer);
@Override
default AiModelType modelType() {
return AiModelType.CHAT;
}
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.AmazonBedrockProviderConfig;
public record AmazonBedrockChatModel(
public record AmazonBedrockChatModelConfig(
AiModelType modelType,
@NotNull @Valid AmazonBedrockProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AmazonBedrockChatModel.Config> {
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {
@Override
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.AnthropicProviderConfig;
public record AnthropicChatModel(
public record AnthropicChatModelConfig(
AiModelType modelType,
@NotNull @Valid AnthropicProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AnthropicChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.AzureOpenAiProviderConfig;
public record AzureOpenAiChatModel(
public record AzureOpenAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid AzureOpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<AzureOpenAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GitHubModelsProviderConfig;
public record GitHubModelsChatModel(
public record GitHubModelsChatModelConfig(
AiModelType modelType,
@NotNull @Valid GitHubModelsProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GitHubModelsChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GoogleAiGeminiProviderConfig;
public record GoogleAiGeminiChatModel(
public record GoogleAiGeminiChatModelConfig(
AiModelType modelType,
@NotNull @Valid GoogleAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GoogleAiGeminiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.GoogleVertexAiGeminiProviderConfig;
public record GoogleVertexAiGeminiChatModel(
public record GoogleVertexAiGeminiChatModelConfig(
AiModelType modelType,
@NotNull @Valid GoogleVertexAiGeminiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<GoogleVertexAiGeminiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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 {
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.MistralAiProviderConfig;
public record MistralAiChatModel(
public record MistralAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid MistralAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<MistralAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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.OpenAiProviderConfig;
public record OpenAiChatModel(
public record OpenAiChatModelConfig(
AiModelType modelType,
@NotNull @Valid OpenAiProviderConfig providerConfig,
@With @NotNull @Valid Config modelConfig
) implements AiChatModel<OpenAiChatModel.Config> {
@NotBlank String modelId,
@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
public AiProvider provider() {
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
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
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),
OTA_PACKAGE(true, EntityType.OTA_PACKAGE),
QUEUE(true, EntityType.QUEUE),
NOTIFICATION_RULE (true, EntityType.NOTIFICATION_RULE),
NOTIFICATION_TARGET (true, EntityType.NOTIFICATION_TARGET),
NOTIFICATION_TEMPLATE (true, EntityType.NOTIFICATION_TEMPLATE),
NOTIFICATION_RULE(true, EntityType.NOTIFICATION_RULE),
NOTIFICATION_TARGET(true, EntityType.NOTIFICATION_TARGET),
NOTIFICATION_TEMPLATE(true, EntityType.NOTIFICATION_TEMPLATE),
TB_RESOURCE(true, EntityType.TB_RESOURCE),
OAUTH2_CLIENT(true, EntityType.OAUTH2_CLIENT),
DOMAIN(true, EntityType.DOMAIN);
DOMAIN(true, EntityType.DOMAIN),
CALCULATED_FIELD(false, EntityType.CALCULATED_FIELD);
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.util.UUID;
public final class AiModelSettingsId extends UUIDBased implements EntityId {
public final class AiModelId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 3021036138554389754L;
@JsonCreator
public AiModelSettingsId(@JsonProperty("id") UUID id) {
public AiModelId(@JsonProperty("id") UUID id) {
super(id);
}
@Override
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
description = "Entity type of the AI model settings",
example = "AI_MODEL_SETTINGS",
allowableValues = "AI_MODEL_SETTINGS"
description = "Entity type of the AI model",
example = "AI_MODEL",
allowableValues = "AI_MODEL"
)
public EntityType getEntityType() {
return EntityType.AI_MODEL_SETTINGS;
return EntityType.AI_MODEL;
}
public static AiModelSettingsId fromString(String uuid) {
return new AiModelSettingsId(UUID.fromString(uuid));
public static AiModelId fromString(String 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_LINK -> new CalculatedFieldLinkId(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!");
};
}
@ -138,6 +138,8 @@ public class EntityIdFactory {
return new OAuth2ClientId(uuid);
case DOMAIN:
return new DomainId(uuid);
case CALCULATED_FIELD:
return new CalculatedFieldId(uuid);
}
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 org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
public class OtaPackageId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator

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

@ -77,16 +77,4 @@ public class RateLimitUtil {
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.DeviceProfile;
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.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.AssetProfile;
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_RULE", value = NotificationRule.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)
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 = "RULE_CHAIN", value = RuleChainExportData.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)
@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.EqualsAndHashCode;
import java.io.Serial;
@EqualsAndHashCode(callSuper = true)
@Data
public class AutoVersionCreateConfig extends VersionCreateConfig {
@Serial
private static final long serialVersionUID = 8245450889383315551L;
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.EntityType;
import org.thingsboard.server.common.data.TenantProfileType;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import org.thingsboard.server.common.data.validation.RateLimit;
import java.io.Serial;
@ -236,43 +235,4 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
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()
.setEdgeRoutingKey(edgeKey)
.setEdgeSecret(edgeSecret)
.setEdgeVersion(EdgeVersion.V_4_0_0)
.setEdgeVersion(EdgeVersion.V_4_1_0)
.setMaxInboundMessageSize(maxInboundMessageSize)
.build())
.build());

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

@ -42,6 +42,7 @@ enum EdgeVersion {
V_3_8_0 = 8;
V_3_9_0 = 9;
V_4_0_0 = 10;
V_4_1_0 = 11;
V_LATEST = 999;
}
@ -124,6 +125,14 @@ enum UpdateMsgType {
// use 6 as a next number
}
message CalculatedFieldUpdateMsg{
UpdateMsgType msgType = 1;
int64 idMSB = 2;
int64 idLSB = 3;
string entity = 4;
}
message EntityDataProto {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
@ -325,6 +334,12 @@ message RelationRequestMsg {
string entityType = 3;
}
message CalculatedFieldRequestMsg {
int64 entityIdMSB = 1;
int64 entityIdLSB = 2;
string entityType = 3;
}
// DEPRECATED. FOR REMOVAL
message UserCredentialsRequestMsg {
option deprecated = true;
@ -423,6 +438,8 @@ message UplinkMsg {
repeated AlarmCommentUpdateMsg alarmCommentUpdateMsg = 22;
repeated RuleChainUpdateMsg ruleChainUpdateMsg = 23;
repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24;
repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25;
repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26;
}
message UplinkResponseMsg {
@ -472,4 +489,5 @@ message DownlinkMsg {
repeated NotificationTargetUpdateMsg notificationTargetUpdateMsg = 32;
repeated NotificationTemplateUpdateMsg notificationTemplateUpdateMsg = 33;
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) {
EdqsResponse response = new EdqsResponse();
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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
@ -538,13 +536,13 @@ public class JsonConverter {
return result;
}
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element) {
public static List<AttributeKvEntry> convertToAttributes(JsonElement element) {
long ts = System.currentTimeMillis();
return convertToAttributes(element, ts);
}
public static Set<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) {
return new HashSet<>(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).toList());
public static List<AttributeKvEntry> convertToAttributes(JsonElement element, long ts) {
return parseValues(element.getAsJsonObject()).stream().<AttributeKvEntry>map(kv -> new BaseAttributeKvEntry(kv, ts)).toList();
}
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) {
TransportProtos.ToDeviceRpcRequestMsg proto = TransportProtos.ToDeviceRpcRequestMsg.newBuilder()
TransportProtos.ToDeviceRpcRequestMsg.Builder builder = TransportProtos.ToDeviceRpcRequestMsg.newBuilder()
.setMethodName(msg.getMsg().getBody().getMethod())
.setParams(msg.getMsg().getBody().getParams())
.setExpirationTime(msg.getMsg().getExpirationTime())
@ -530,7 +530,11 @@ public class ProtoUtils {
.setRequestIdLSB(msg.getMsg().getId().getLeastSignificantBits())
.setOneway(msg.getMsg().isOneway())
.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()
.setTenantIdMSB(msg.getTenantId().getId().getMostSignificantBits())
@ -551,7 +555,7 @@ public class ProtoUtils {
toDeviceRpcRequestMsg.getOneway(),
toDeviceRpcRequestMsg.getExpirationTime(),
new ToDeviceRpcRequestBody(toDeviceRpcRequestMsg.getMethodName(), toDeviceRpcRequestMsg.getParams()),
toDeviceRpcRequestMsg.getPersisted(), 0, "");
toDeviceRpcRequestMsg.getPersisted(), toDeviceRpcRequestMsg.hasRetries() ? toDeviceRpcRequestMsg.getRetries() : null, toDeviceRpcRequestMsg.getAdditionalInfo());
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_LINK = 40;
JOB = 41;
AI_MODEL_SETTINGS = 42;
AI_MODEL = 42;
}
enum ApiUsageRecordKeyProto {
@ -697,6 +697,8 @@ message ToDeviceRpcRequestMsg {
int64 requestIdLSB = 6;
bool oneway = 7;
bool persisted = 8;
optional int32 retries = 9;
string additionalInfo = 10;
}
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.parallel.Isolated;
import java.util.ArrayList;
@Isolated("JsonConverter static settings being modified")
public class JsonConverterTest {
@ -53,7 +51,7 @@ public class JsonConverterTest {
@Test
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());
}
@ -108,4 +106,5 @@ public class JsonConverterTest {
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.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.MessagesStats;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.TbQueueHandler;
import org.thingsboard.server.queue.TbQueueMsg;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager;
@ -119,8 +121,20 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
response -> {
pendingRequestCount.decrementAndGet();
response.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId));
responseProducer.send(TopicPartitionInfo.builder().topic(responseTopic).build(), response, null);
stats.incrementSuccessful();
TopicPartitionInfo tpi = TopicPartitionInfo.builder().topic(responseTopic).build();
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 -> {
pendingRequestCount.decrementAndGet();
@ -144,6 +158,15 @@ public class PartitionedQueueResponseTemplate<Request extends TbQueueMsg, Respon
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) {
requestConsumer.update(partitions);
}

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

Loading…
Cancel
Save