Browse Source

Merge branch 'rc' into features/add_tooltip_option_to_show_stack_mode_total_value_on_timeseries_chart_widgets

pull/13373/head
Paolo Cristiani 1 year ago
committed by GitHub
parent
commit
633b2c8a2b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 44
      application/pom.xml
  2. 14
      application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md
  3. 75
      application/src/main/data/upgrade/basic/schema_update.sql
  4. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  5. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  6. 12
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  7. 20
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  8. 61
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  9. 178
      application/src/main/java/org/thingsboard/server/controller/AiModelController.java
  10. 16
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  11. 60
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  12. 22
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  13. 109
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  14. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  15. 15
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  16. 35
      application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java
  17. 163
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  18. 133
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  19. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  20. 20
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java
  21. 40
      application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java
  22. 27
      application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java
  23. 86
      application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java
  24. 269
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java
  26. 8
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java
  28. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  29. 80
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java
  30. 27
      application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
  32. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/ota/DefaultTbOtaPackageService.java
  33. 43
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java
  34. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  35. 101
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  36. 183
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  37. 22
      application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java
  38. 19
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  39. 23
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  40. 3
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  41. 7
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  42. 17
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  43. 16
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
  44. 12
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  45. 12
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java
  46. 5
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
  47. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  48. 36
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java
  49. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceExportService.java
  50. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DeviceProfileExportService.java
  51. 49
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/OtaPackageExportService.java
  52. 3
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  53. 77
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java
  54. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  55. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java
  56. 13
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceProfileImportService.java
  57. 76
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/OtaPackageImportService.java
  58. 7
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  59. 4
      application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java
  60. 6
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  61. 50
      application/src/main/resources/thingsboard.yml
  62. 615
      application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java
  63. 278
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  64. 16
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  65. 7
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  66. 33
      application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java
  67. 268
      application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java
  68. 101
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  69. 316
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java
  70. 29
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  71. 80
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  72. 5
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java
  73. 3
      application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java
  74. 102
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  75. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  76. 26
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java
  77. 31
      application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java
  78. 7
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java
  79. 103
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java
  80. 73
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java
  81. 4
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java
  82. 6
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java
  83. 39
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java
  84. 9
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java
  85. 6
      application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java
  86. 2
      common/actor/pom.xml
  87. 2
      common/cache/pom.xml
  88. 2
      common/cluster-api/pom.xml
  89. 7
      common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueHandler.java
  90. 2
      common/coap-server/pom.xml
  91. 5
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java
  92. 5
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java
  93. 2
      common/dao-api/pom.xml
  94. 42
      common/dao-api/src/main/java/org/thingsboard/server/dao/ai/AiModelService.java
  95. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  96. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/ota/OtaPackageService.java
  97. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java
  98. 6
      common/data/pom.xml
  99. 6
      common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java
  100. 7
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

44
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>application</artifactId> <artifactId>application</artifactId>
@ -186,8 +186,8 @@
<artifactId>jjwt</artifactId> <artifactId>jjwt</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.freemarker</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>freemarker</artifactId> <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
@ -381,6 +381,44 @@
<groupId>org.rocksdb</groupId> <groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId> <artifactId>rocksdbjni</artifactId>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
<artifactId>azure-core-test</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
<build> <build>

14
application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_preparing.md

@ -32,19 +32,7 @@ docker run --rm -v tb-edge-postgres-data:/volume -v ~/.mytb-edge-data/db:/backup
After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration. After completing the data migration to the newly created Docker volumes, you'll need to update the volume mounts in your Docker Compose configuration.
Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings. Modify the `docker-compose.yml` file for ThingsBoard Edge to update the volume settings.
First, please update docker compose file version. Find next snippet: Update volume mounts. Locate the following snippet:
```text
version: '3.0'
...
```
And replace it with:
```text
version: '3.8'
...
```
Then update volume mounts. Locate the following snippet:
```text ```text
volumes: volumes:
- ~/.mytb-edge-data:/data - ~/.mytb-edge-data:/data

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

@ -14,48 +14,33 @@
-- limitations under the License. -- limitations under the License.
-- --
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS START -- UPDATE OTA PACKAGE EXTERNAL ID START
UPDATE tenant_profile ALTER TABLE ota_package
SET profile_data = jsonb_set( ADD COLUMN IF NOT EXISTS external_id uuid;
profile_data,
'{configuration}', DO
( $$
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration' BEGIN
|| IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN
COALESCE( ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
CASE END IF;
WHEN profile_data -> 'configuration' -> END;
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN $$;
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits', -- UPDATE OTA PACKAGE EXTERNAL ID END
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits', -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits', DROP INDEX IF EXISTS idx_device_external_id;
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration', DROP INDEX IF EXISTS idx_device_profile_external_id;
'cassandraWriteQueryTenantRuleEngineRateLimits', DROP INDEX IF EXISTS idx_asset_external_id;
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration' DROP INDEX IF EXISTS idx_entity_view_external_id;
) DROP INDEX IF EXISTS idx_rule_chain_external_id;
END, DROP INDEX IF EXISTS idx_dashboard_external_id;
'{}'::jsonb DROP INDEX IF EXISTS idx_customer_external_id;
) DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
)
) -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';
ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255);
-- UPDATE TENANT PROFILE CASSANDRA RATE LIMITS END
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS 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"%';
-- UPDATE NOTIFICATION RULE CASSANDRA RATE LIMITS END

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

@ -35,6 +35,7 @@ import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings; import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.notification.SlackService; import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
@ -62,6 +63,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -311,6 +313,14 @@ public class ActorSystemContext {
@Getter @Getter
private AuditLogService auditLogService; private AuditLogService auditLogService;
@Autowired
@Getter
private RuleEngineAiChatModelService aiChatModelService;
@Autowired
@Getter
private AiModelService aiModelService;
@Autowired @Autowired
@Getter @Getter
private EntityViewService entityViewService; private EntityViewService entityViewService;

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

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

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

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

20
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -26,9 +26,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@ -183,15 +181,12 @@ public class ThingsboardSecurityConfiguration {
} }
@Bean @Bean
public AuthenticationManager authenticationManager(ObjectPostProcessor<Object> objectPostProcessor) throws Exception { public AuthenticationManager authenticationManager() {
DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor return new ProviderManager(List.of(
.postProcess(new DefaultAuthenticationEventPublisher()); restAuthenticationProvider,
var auth = new AuthenticationManagerBuilder(objectPostProcessor); jwtAuthenticationProvider,
auth.authenticationEventPublisher(eventPublisher); refreshTokenAuthenticationProvider
auth.authenticationProvider(restAuthenticationProvider); ));
auth.authenticationProvider(jwtAuthenticationProvider);
auth.authenticationProvider(refreshTokenAuthenticationProvider);
return auth.build();
} }
@Autowired @Autowired
@ -265,4 +260,5 @@ public class ThingsboardSecurityConfiguration {
return new CorsFilter(source); return new CorsFilter(source);
} }
} }
} }

61
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -37,14 +37,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
@ -125,14 +124,13 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) @GetMapping(value = "/settings/{key}")
@ResponseBody
public AdminSettings getAdminSettings( public AdminSettings getAdminSettings(
@Parameter(description = "A string value of the key (e.g. 'general' or 'mail').") @Parameter(description = "A string value of the key (e.g. 'general' or 'mail').")
@PathVariable("key") String key) throws ThingsboardException { @PathVariable("key") String key) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key);
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
} }
@ -144,15 +142,14 @@ public class AdminController extends BaseController {
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
"Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings", method = RequestMethod.POST) @PostMapping(value = "/settings")
@ResponseBody
public AdminSettings saveAdminSettings( public AdminSettings saveAdminSettings(
@Parameter(description = "A JSON value representing the Administration Settings.") @Parameter(description = "A JSON value representing the Administration Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException { @RequestBody AdminSettings adminSettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
adminSettings.setTenantId(getTenantId()); adminSettings.setTenantId(getTenantId());
adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
mailService.updateMailConfiguration(); mailService.updateMailConfiguration();
((ObjectNode) adminSettings.getJsonValue()).remove("password"); ((ObjectNode) adminSettings.getJsonValue()).remove("password");
((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken"); ((ObjectNode) adminSettings.getJsonValue()).remove("refreshToken");
@ -165,8 +162,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the Security Settings object (getSecuritySettings)", @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)",
notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.GET) @GetMapping(value = "/securitySettings")
@ResponseBody
public SecuritySettings getSecuritySettings() throws ThingsboardException { public SecuritySettings getSecuritySettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(securitySettingsService.getSecuritySettings()); return checkNotNull(securitySettingsService.getSecuritySettings());
@ -175,8 +171,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Update Security Settings (saveSecuritySettings)", @ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.POST) @PostMapping(value = "/securitySettings")
@ResponseBody
public SecuritySettings saveSecuritySettings( public SecuritySettings saveSecuritySettings(
@Parameter(description = "A JSON value representing the Security Settings.") @Parameter(description = "A JSON value representing the Security Settings.")
@RequestBody SecuritySettings securitySettings) throws ThingsboardException { @RequestBody SecuritySettings securitySettings) throws ThingsboardException {
@ -188,8 +183,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)", @ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET) @GetMapping(value = "/jwtSettings")
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException { public JwtSettings getJwtSettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings()); return checkNotNull(jwtSettingsService.getJwtSettings());
@ -198,8 +192,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)", @ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST) @PostMapping(value = "/jwtSettings")
@ResponseBody
public JwtPair saveJwtSettings( public JwtPair saveJwtSettings(
@Parameter(description = "A JSON value representing the JWT Settings.") @Parameter(description = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException { @RequestBody JwtSettings jwtSettings) throws ThingsboardException {
@ -213,15 +206,15 @@ public class AdminController extends BaseController {
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) @PostMapping(value = "/settings/testMail")
public void sendTestMail( public void sendTestMail(
@Parameter(description = "A JSON value representing the Mail Settings.") @Parameter(description = "A JSON value representing the Mail Settings.")
@RequestBody AdminSettings adminSettings) throws ThingsboardException { @RequestBody AdminSettings adminSettings) throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
adminSettings = checkNotNull(adminSettings); adminSettings = checkNotNull(adminSettings);
if (adminSettings.getKey().equals("mail")) { if (adminSettings.getKey().equals(MAIL_SETTINGS_KEY)) {
if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) { if (adminSettings.getJsonValue().has("enableOauth2") && adminSettings.getJsonValue().get("enableOauth2").asBoolean()) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY));
JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken"); JsonNode refreshToken = mailSettings.getJsonValue().get("refreshToken");
if (refreshToken == null) { if (refreshToken == null) {
throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL); throw new ThingsboardException("Refresh token was not generated. Please, generate refresh token.", ThingsboardErrorCode.GENERAL);
@ -230,7 +223,7 @@ public class AdminController extends BaseController {
settings.put("refreshToken", refreshToken.asText()); settings.put("refreshToken", refreshToken.asText());
} else { } else {
if (!adminSettings.getJsonValue().has("password")) { if (!adminSettings.getJsonValue().has("password")) {
AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, MAIL_SETTINGS_KEY));
((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
} }
} }
@ -251,7 +244,7 @@ public class AdminController extends BaseController {
notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) @PostMapping(value = "/settings/testSms")
public void sendTestSms( public void sendTestSms(
@Parameter(description = "A JSON value representing the Test SMS request.") @Parameter(description = "A JSON value representing the Test SMS request.")
@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
@ -325,7 +318,7 @@ public class AdminController extends BaseController {
notes = "Deletes the repository settings." notes = "Deletes the repository settings."
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings", method = RequestMethod.DELETE) @DeleteMapping(value = "/repositorySettings")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public DeferredResult<Void> deleteRepositorySettings() throws Exception { public DeferredResult<Void> deleteRepositorySettings() throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
@ -335,7 +328,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Check repository access (checkRepositoryAccess)", @ApiOperation(value = "Check repository access (checkRepositoryAccess)",
notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Attempts to check repository access. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/repositorySettings/checkAccess", method = RequestMethod.POST) @PostMapping(value = "/repositorySettings/checkAccess")
public DeferredResult<Void> checkRepositoryAccess( public DeferredResult<Void> checkRepositoryAccess(
@Parameter(description = "A JSON value representing the Repository Settings.") @Parameter(description = "A JSON value representing the Repository Settings.")
@RequestBody RepositorySettings settings) throws Exception { @RequestBody RepositorySettings settings) throws Exception {
@ -376,7 +369,7 @@ public class AdminController extends BaseController {
notes = "Deletes the auto commit settings." notes = "Deletes the auto commit settings."
+ TENANT_AUTHORITY_PARAGRAPH) + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/autoCommitSettings", method = RequestMethod.DELETE) @DeleteMapping(value = "/autoCommitSettings")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public void deleteAutoCommitSettings() throws ThingsboardException { public void deleteAutoCommitSettings() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE); accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.DELETE);
@ -387,9 +380,8 @@ public class AdminController extends BaseController {
notes = "Check notifications about new platform releases. " notes = "Check notifications about new platform releases. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/updates", method = RequestMethod.GET) @GetMapping(value = "/updates")
@ResponseBody public UpdateMessage checkUpdates() {
public UpdateMessage checkUpdates() throws ThingsboardException {
return updateService.checkUpdates(); return updateService.checkUpdates();
} }
@ -397,9 +389,8 @@ public class AdminController extends BaseController {
notes = "Get main information about system. " notes = "Get main information about system. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/systemInfo", method = RequestMethod.GET) @GetMapping(value = "/systemInfo")
@ResponseBody public SystemInfo getSystemInfo() {
public SystemInfo getSystemInfo() throws ThingsboardException {
return systemInfoService.getSystemInfo(); return systemInfoService.getSystemInfo();
} }
@ -407,8 +398,7 @@ public class AdminController extends BaseController {
notes = "Get information about enabled/disabled features. " notes = "Get information about enabled/disabled features. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/featuresInfo", method = RequestMethod.GET) @GetMapping(value = "/featuresInfo")
@ResponseBody
public FeaturesInfo getFeaturesInfo() { public FeaturesInfo getFeaturesInfo() {
return systemInfoService.getFeaturesInfo(); return systemInfoService.getFeaturesInfo();
} }
@ -417,8 +407,7 @@ public class AdminController extends BaseController {
"double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " + "double quotes. After successful authentication with OAuth2 provider and user consent for requested scope, it makes a redirect to this path so that the platform can do " +
"further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH) "further log in processing and generating access tokens. " + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/loginProcessingUrl", method = RequestMethod.GET) @GetMapping(value = "/mail/oauth2/loginProcessingUrl")
@ResponseBody
public String getMailProcessingUrl() throws ThingsboardException { public String getMailProcessingUrl() throws ThingsboardException {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return "\"/api/admin/mail/oauth2/code\""; return "\"/api/admin/mail/oauth2/code\"";
@ -427,7 +416,7 @@ public class AdminController extends BaseController {
@ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" + @ApiOperation(value = "Redirect user to mail provider login page. ", notes = "After user logged in and provided access" +
"provider sends authorization code to specified redirect uri.)") "provider sends authorization code to specified redirect uri.)")
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/mail/oauth2/authorize", method = RequestMethod.GET, produces = "application/text") @GetMapping(value = "/mail/oauth2/authorize", produces = "application/text")
public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException { public String getAuthorizationUrl(HttpServletRequest request, HttpServletResponse response) throws ThingsboardException {
String state = StringUtils.generateSafeToken(); String state = StringUtils.generateSafeToken();
if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) { if (request.getParameter(PREV_URI_PATH_PARAMETER) != null) {
@ -452,7 +441,7 @@ public class AdminController extends BaseController {
.build() + "\""; .build() + "\"";
} }
@RequestMapping(value = "/mail/oauth2/code", params = {"code", "state"}, method = RequestMethod.GET) @GetMapping(value = "/mail/oauth2/code", params = {"code", "state"})
public void codeProcessingUrl( public void codeProcessingUrl(
@RequestParam(value = "code") String code, @RequestParam(value = "state") String state, @RequestParam(value = "code") String code, @RequestParam(value = "state") String state,
HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException { HttpServletRequest request, HttpServletResponse response) throws ThingsboardException, IOException {

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

@ -0,0 +1,178 @@
/**
* 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 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.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.queue.util.TbCoreComponent;
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
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api/ai/model")
class AiModelController extends BaseController {
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)",
notes = "Submits a single prompt - made up of an optional system message and a required user message - to the specified AI chat model " +
"and returns either the generated answer or an error envelope." +
TENANT_AUTHORITY_PARAGRAPH
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/chat")
public DeferredResult<TbChatResponse> sendChatRequest(@Valid @RequestBody TbChatRequest tbChatRequest) {
ChatRequest langChainChatRequest = tbChatRequest.toLangChainChatRequest();
AiChatModelConfig<?> chatModelConfig = tbChatRequest.chatModelConfig();
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 = chatModelConfig.timeoutSeconds();
return requestTimeoutSeconds != null ? wrapFuture(future, Duration.ofSeconds(requestTimeoutSeconds).toMillis()) : wrapFuture(future);
}
}

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

@ -19,12 +19,13 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
@ -54,6 +55,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api") @RequestMapping("/api")
public class AlarmCommentController extends BaseController { public class AlarmCommentController extends BaseController {
public static final String ALARM_ID = "alarmId"; public static final String ALARM_ID = "alarmId";
public static final String ALARM_COMMENT_ID = "commentId"; public static final String ALARM_COMMENT_ID = "commentId";
@ -68,8 +70,7 @@ public class AlarmCommentController extends BaseController {
"\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -82,8 +83,7 @@ public class AlarmCommentController extends BaseController {
@ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)",
notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/comment/{commentId}")
@ResponseBody
public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -98,8 +98,7 @@ public class AlarmCommentController extends BaseController {
notes = "Returns a page of alarm comments for specified alarm. " + notes = "Returns a page of alarm comments for specified alarm. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public PageData<AlarmCommentInfo> getAlarmComments( public PageData<AlarmCommentInfo> getAlarmComments(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -118,4 +117,5 @@ public class AlarmCommentController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink));
} }
} }

60
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -21,12 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
@ -58,7 +59,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
@ -104,8 +104,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm (getAlarmById)", @ApiOperation(value = "Get Alarm (getAlarmById)",
notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK) notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}")
@ResponseBody
public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -117,8 +116,7 @@ public class AlarmController extends BaseController {
notes = "Fetch the Alarm Info object based on the provided Alarm Id. " + notes = "Fetch the Alarm Info object based on the provided Alarm Id. " +
ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/info/{alarmId}")
@ResponseBody
public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -136,11 +134,9 @@ public class AlarmController extends BaseController {
"If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " + "If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " +
"If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " + "If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm", method = RequestMethod.POST) @PostMapping(value = "/alarm")
@ResponseBody
public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
alarm.setTenantId(getTenantId()); alarm.setTenantId(getTenantId());
checkNotNull(alarm.getOriginator()); checkNotNull(alarm.getOriginator());
@ -155,8 +151,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Delete Alarm (deleteAlarm)", @ApiOperation(value = "Delete Alarm (deleteAlarm)",
notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}")
@ResponseBody
public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -169,7 +164,7 @@ public class AlarmController extends BaseController {
"Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " + "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/ack")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -184,7 +179,7 @@ public class AlarmController extends BaseController {
"Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " + "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/clear")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -200,7 +195,7 @@ public class AlarmController extends BaseController {
"(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/assign/{assigneeId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -221,7 +216,7 @@ public class AlarmController extends BaseController {
"Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/assign")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId @PathVariable(ALARM_ID) String strAlarmId
@ -236,8 +231,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarms( public PageData<AlarmInfo> getAlarms(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -265,7 +259,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -292,8 +286,7 @@ public class AlarmController extends BaseController {
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarms", method = RequestMethod.GET) @GetMapping(value = "/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarms( public PageData<AlarmInfo> getAllAlarms(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ -317,7 +310,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) { if (alarmSearchStatus != null && alarmStatus != null) {
@ -341,8 +334,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. " + notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/v2/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2( public PageData<AlarmInfo> getAlarmsV2(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -370,7 +362,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -407,8 +399,7 @@ public class AlarmController extends BaseController {
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) @GetMapping(value = "/v2/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2( public PageData<AlarmInfo> getAllAlarmsV2(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})))
@RequestParam(required = false) String[] statusList, @RequestParam(required = false) String[] statusList,
@ -432,7 +423,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>(); List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) { if (statusList != null) {
for (String strStatus : statusList) { for (String strStatus : statusList) {
@ -465,11 +456,9 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}")
@ResponseBody
public AlarmSeverity getHighestAlarmSeverity( public AlarmSeverity getHighestAlarmSeverity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -499,8 +488,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm Types (getAlarmTypes)", @ApiOperation(value = "Get Alarm Types (getAlarmTypes)",
notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.") notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/types", method = RequestMethod.GET) @GetMapping(value = "/alarm/types")
@ResponseBody
public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -508,7 +496,7 @@ public class AlarmController extends BaseController {
@Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) @Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder);
return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink));
} }

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

@ -37,6 +37,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
@ -61,6 +62,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -75,6 +77,7 @@ import org.thingsboard.server.common.data.edge.EdgeInfo;
import org.thingsboard.server.common.data.exception.EntityVersionMismatchException; import org.thingsboard.server.common.data.exception.EntityVersionMismatchException;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmCommentId; import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
@ -129,6 +132,7 @@ import org.thingsboard.server.common.data.util.ThrowingBiFunction;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
@ -175,6 +179,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService; import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.entitiy.ai.TbAiModelService;
import org.thingsboard.server.service.entitiy.user.TbUserSettingsService; import org.thingsboard.server.service.entitiy.user.TbUserSettingsService;
import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache; import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -378,6 +383,12 @@ public abstract class BaseController {
@Autowired @Autowired
protected CalculatedFieldService calculatedFieldService; protected CalculatedFieldService calculatedFieldService;
@Autowired
protected AiModelService aiModelService;
@Autowired
protected TbAiModelService tbAiModelService;
@Value("${server.log_controller_error_stack_trace}") @Value("${server.log_controller_error_stack_trace}")
@Getter @Getter
private boolean logControllerErrorStackTrace; private boolean logControllerErrorStackTrace;
@ -390,7 +401,7 @@ public abstract class BaseController {
public void handleControllerException(Exception e, HttpServletResponse response) { public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e); ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) { && StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause(); e = (Exception) thingsboardException.getCause();
} else { } else {
e = thingsboardException; e = thingsboardException;
@ -438,7 +449,7 @@ public abstract class BaseController {
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) { || exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (exception instanceof MessagingException) { } else if (exception instanceof MessagingException) {
return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL); return new ThingsboardException("Unable to send mail", ThingsboardErrorCode.GENERAL);
@ -448,6 +459,8 @@ public abstract class BaseController {
return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE); return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE);
} else if (exception instanceof EntityVersionMismatchException) { } else if (exception instanceof EntityVersionMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT);
} else if (exception instanceof MethodArgumentTypeMismatchException) {
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL);
} }
@ -634,6 +647,7 @@ public abstract class BaseController {
case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation); case MOBILE_APP -> checkMobileAppId(new MobileAppId(entityId.getId()), operation);
case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation); case MOBILE_APP_BUNDLE -> checkMobileAppBundleId(new MobileAppBundleId(entityId.getId()), operation);
case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation); case CALCULATED_FIELD -> checkCalculatedFieldId(new CalculatedFieldId(entityId.getId()), operation);
case AI_MODEL -> checkAiModelId(new AiModelId(entityId.getId()), operation);
default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation); default -> (HasId<? extends EntityId>) checkEntityId(entityId, entitiesService::findEntityByTenantIdAndId, operation);
}; };
} catch (Exception e) { } catch (Exception e) {
@ -837,6 +851,10 @@ public abstract class BaseController {
return checkEntityId(jobId, jobService::findJobById, operation); return checkEntityId(jobId, jobService::findJobById, 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) { protected <I extends EntityId> I emptyId(EntityType entityType) {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
} }

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

@ -17,19 +17,21 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -41,7 +43,6 @@ import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.EventType;
@ -49,17 +50,14 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldScriptEngine;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList; import java.util.ArrayList;
@ -100,30 +98,30 @@ public class CalculatedFieldController extends BaseController {
private static final String TEST_SCRIPT_EXPRESSION = private static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n" "Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START + MARKDOWN_CODE_BLOCK_START
+ "{\n" + + "{\n" +
" \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" + " \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" +
" \"arguments\": {\n" + " \"arguments\": {\n" +
" \"temperature\": {\n" + " \"temperature\": {\n" +
" \"type\": \"TS_ROLLING\",\n" + " \"type\": \"TS_ROLLING\",\n" +
" \"timeWindow\": {\n" + " \"timeWindow\": {\n" +
" \"startTs\": 1739775630002,\n" + " \"startTs\": 1739775630002,\n" +
" \"endTs\": 65432211,\n" + " \"endTs\": 65432211,\n" +
" \"limit\": 5\n" + " \"limit\": 5\n" +
" },\n" + " },\n" +
" \"values\": [\n" + " \"values\": [\n" +
" { \"ts\": 1739775639851, \"value\": 23 },\n" + " { \"ts\": 1739775639851, \"value\": 23 },\n" +
" { \"ts\": 1739775664561, \"value\": 43 },\n" + " { \"ts\": 1739775664561, \"value\": 43 },\n" +
" { \"ts\": 1739775713079, \"value\": 15 },\n" + " { \"ts\": 1739775713079, \"value\": 15 },\n" +
" { \"ts\": 1739775999522, \"value\": 34 },\n" + " { \"ts\": 1739775999522, \"value\": 34 },\n" +
" { \"ts\": 1739776228452, \"value\": 22 }\n" + " { \"ts\": 1739776228452, \"value\": 22 }\n" +
" ]\n" + " ]\n" +
" },\n" + " },\n" +
" \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" + " \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\n" +
" }\n" + " }\n" +
"}" "}"
+ MARKDOWN_CODE_BLOCK_END + MARKDOWN_CODE_BLOCK_END
+ "\n\n Expected result JSON contains \"output\" and \"error\"."; + "\n\n Expected result JSON contains \"output\" and \"error\".";
@ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)", @ApiOperation(value = "Create Or Update Calculated Field (saveCalculatedField)",
notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK + notes = "Creates or Updates the Calculated Field. When creating calculated field, platform generates Calculated Field Id as " + UUID_WIKI_LINK +
@ -133,13 +131,12 @@ public class CalculatedFieldController extends BaseController {
"Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. " "Remove 'id', 'tenantId' from the request body example (below) to create new Calculated Field entity. "
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField", method = RequestMethod.POST) @PostMapping("/calculatedField")
@ResponseBody
public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.") public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.")
@RequestBody CalculatedField calculatedField) throws Exception { @RequestBody CalculatedField calculatedField) throws Exception {
calculatedField.setTenantId(getTenantId()); calculatedField.setTenantId(getTenantId());
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser()); checkReferencedEntities(calculatedField.getConfiguration());
return tbCalculatedFieldService.save(calculatedField, getCurrentUser()); return tbCalculatedFieldService.save(calculatedField, getCurrentUser());
} }
@ -147,8 +144,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Fetch the Calculated Field object based on the provided Calculated Field Id." notes = "Fetch the Calculated Field object based on the provided Calculated Field Id."
) )
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.GET) @GetMapping("/calculatedField/{calculatedFieldId}")
@ResponseBody
public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { public CalculatedField getCalculatedFieldById(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -162,8 +158,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Fetch the Calculated Fields based on the provided Entity Id." notes = "Fetch the Calculated Fields based on the provided Entity Id."
) )
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"})
@ResponseBody
public PageData<CalculatedField> getCalculatedFieldsByEntityId( public PageData<CalculatedField> getCalculatedFieldsByEntityId(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ -182,8 +177,8 @@ public class CalculatedFieldController extends BaseController {
@ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)", @ApiOperation(value = "Delete Calculated Field (deleteCalculatedField)",
notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the calculated field. Referencing non-existing Calculated Field Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}", method = RequestMethod.DELETE) @DeleteMapping("/calculatedField/{calculatedFieldId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -196,8 +191,7 @@ public class CalculatedFieldController extends BaseController {
notes = "Gets latest calculated field debug event for specified calculated field id. " + notes = "Gets latest calculated field debug event for specified calculated field id. " +
"Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) "Referencing non-existing calculated field id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/{calculatedFieldId}/debug", method = RequestMethod.GET) @GetMapping("/calculatedField/{calculatedFieldId}/debug")
@ResponseBody
public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException {
checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId));
@ -212,15 +206,13 @@ public class CalculatedFieldController extends BaseController {
@ApiOperation(value = "Test Script expression", @ApiOperation(value = "Test Script expression",
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/calculatedField/testScript", method = RequestMethod.POST) @PostMapping("/calculatedField/testScript")
@ResponseBody
public JsonNode testScript( public JsonNode testScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) { @RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText(); String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse( Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() { JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
}),
Collections.emptyMap() Collections.emptyMap()
); );
@ -231,12 +223,13 @@ public class CalculatedFieldController extends BaseController {
String output = ""; String output = "";
String errorText = ""; String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try { try {
if (tbelInvokeService == null) { if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!"); throw new IllegalArgumentException("TBEL script engine is disabled!");
} }
CalculatedFieldScriptEngine calculatedFieldScriptEngine = new CalculatedFieldTbelScriptEngine( engine = new CalculatedFieldTbelScriptEngine(
getTenantId(), getTenantId(),
tbelInvokeService, tbelInvokeService,
expression, expression,
@ -254,17 +247,20 @@ public class CalculatedFieldController extends BaseController {
} }
} }
JsonNode json = calculatedFieldScriptEngine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS); JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json); output = JacksonUtil.toString(json);
} catch (Exception e) { } catch (Exception e) {
log.error("Error evaluating expression", e); log.error("Error evaluating expression", e);
errorText = e.getMessage(); Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
} }
return JacksonUtil.newObjectNode()
ObjectNode result = JacksonUtil.newObjectNode(); .put("output", output)
result.put("output", output); .put("error", errorText);
result.put("error", errorText);
return result;
} }
private long getLatestTimestamp(Map<String, TbelCfArg> arguments) { private long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
@ -281,7 +277,7 @@ public class CalculatedFieldController extends BaseController {
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp; return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
} }
private <E extends HasId<I> & HasTenantId, I extends EntityId> void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig, SecurityUser user) throws ThingsboardException { private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities(); List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) { for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType(); EntityType entityType = referencedEntityId.getEntityType();
@ -290,8 +286,7 @@ public class CalculatedFieldController extends BaseController {
return; return;
} }
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
} }
} }

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

@ -31,7 +31,7 @@ public class ControllerConstants {
protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String ASSIGNEE_ID = "assigneeId";
protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
"The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
"See the 'Model' tab of the Response Class for more details. "; "See response schema for more details. ";
protected static final String INLINE_IMAGES = "inlineImages"; protected static final String INLINE_IMAGES = "inlineImages";
protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)"; protected static final String INLINE_IMAGES_DESCRIPTION = "Inline images as a data URL (Base64)";
@ -90,6 +90,7 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name."; protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name."; protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name."; protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String AI_MODEL_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the AI model name, provider and model ID.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name."; protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title."; protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";

15
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService; import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.config.annotations.ApiOperation;
@ -76,6 +77,7 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.") @Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.")
@RequestBody EntityCountQuery query) throws ThingsboardException { @RequestBody EntityCountQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
} }
@ -87,6 +89,7 @@ public class EntityQueryController extends BaseController {
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.") @Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@RequestBody EntityDataQuery query) throws ThingsboardException { @RequestBody EntityDataQuery query) throws ThingsboardException {
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
} }
@ -103,6 +106,7 @@ public class EntityQueryController extends BaseController {
if (assigneeId != null) { if (assigneeId != null) {
checkUserId(assigneeId, Operation.READ); checkUserId(assigneeId, Operation.READ);
} }
resolveQuery(query);
return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
} }
@ -117,6 +121,7 @@ public class EntityQueryController extends BaseController {
if (assigneeId != null) { if (assigneeId != null) {
checkUserId(assigneeId, Operation.READ); checkUserId(assigneeId, Operation.READ);
} }
resolveQuery(query);
return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query); return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
} }
@ -136,6 +141,7 @@ public class EntityQueryController extends BaseController {
@RequestParam(value = "scope", required = false) String scope) throws ThingsboardException { @RequestParam(value = "scope", required = false) String scope) throws ThingsboardException {
TenantId tenantId = getTenantId(); TenantId tenantId = getTenantId();
checkNotNull(query); checkNotNull(query);
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink(); EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) { if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE); pageLink.setPageSize(MAX_PAGE_SIZE);
@ -155,4 +161,13 @@ public class EntityQueryController extends BaseController {
return edqsService.getState(); return edqsService.getState();
} }
private void resolveQuery(EntityCountQuery query) throws ThingsboardException {
if (query.getEntityFilter() != null) {
var user = getCurrentUser();
var customerId = user.getCustomerId();
var ownerId = customerId != null && !customerId.isNullUid() ? customerId : getTenantId();
EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), user.getId(), ownerId);
}
}
} }

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

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

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

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
@ -23,16 +22,19 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
@ -155,9 +157,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain getRuleChainById( public RuleChain getRuleChainById(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -169,9 +170,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)",
notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. " notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. "
+ RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/output/labels")
@ResponseBody
public Set<String> getRuleChainOutputLabels( public Set<String> getRuleChainOutputLabels(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -184,9 +184,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)",
notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. " notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. "
+ RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels/usage", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/output/labels/usage")
@ResponseBody
public List<RuleChainOutputLabelsUsage> getRuleChainOutputLabelsUsage( public List<RuleChainOutputLabelsUsage> getRuleChainOutputLabelsUsage(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -198,9 +197,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chain (getRuleChainById)", @ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET) @GetMapping("/ruleChain/{ruleChainId}/metadata")
@ResponseBody
public RuleChainMetaData getRuleChainMetaData( public RuleChainMetaData getRuleChainMetaData(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -218,9 +216,8 @@ public class RuleChainController extends BaseController {
"\n\n" + RULE_CHAIN_DESCRIPTION + "\n\n" + RULE_CHAIN_DESCRIPTION +
"Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." + "Remove 'id', 'tenantId' from the request body example (below) to create new Rule Chain entity." +
TENANT_AUTHORITY_PARAGRAPH) TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain", method = RequestMethod.POST) @PostMapping("/ruleChain")
@ResponseBody
public RuleChain saveRuleChain( public RuleChain saveRuleChain(
@Parameter(description = "A JSON value representing the rule chain.") @Parameter(description = "A JSON value representing the rule chain.")
@RequestBody RuleChain ruleChain) throws Exception { @RequestBody RuleChain ruleChain) throws Exception {
@ -232,9 +229,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Create Default Rule Chain", @ApiOperation(value = "Create Default Rule Chain",
notes = "Create rule chain from template, based on the specified name in the request. " + notes = "Create rule chain from template, based on the specified name in the request. " +
"Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) @PostMapping("/ruleChain/device/default")
@ResponseBody
public RuleChain saveRuleChain( public RuleChain saveRuleChain(
@Parameter(description = "A JSON value representing the request.") @Parameter(description = "A JSON value representing the request.")
@RequestBody DefaultRuleChainCreateRequest request) throws Exception { @RequestBody DefaultRuleChainCreateRequest request) throws Exception {
@ -245,9 +241,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)", @ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)",
notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH) notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/root")
@ResponseBody
public RuleChain setRootRuleChain( public RuleChain setRootRuleChain(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -259,9 +254,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Update Rule Chain Metadata", @ApiOperation(value = "Update Rule Chain Metadata",
notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST) @PostMapping("/ruleChain/metadata")
@ResponseBody
public RuleChainMetaData saveRuleChainMetaData( public RuleChainMetaData saveRuleChainMetaData(
@Parameter(description = "A JSON value representing the rule chain metadata.") @Parameter(description = "A JSON value representing the rule chain metadata.")
@RequestBody RuleChainMetaData ruleChainMetaData, @RequestBody RuleChainMetaData ruleChainMetaData,
@ -284,8 +278,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Rule Chains (getRuleChains)", @ApiOperation(value = "Get Rule Chains (getRuleChains)",
notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/ruleChains", params = {"pageSize", "page"})
@ResponseBody
public PageData<RuleChain> getRuleChains( public PageData<RuleChain> getRuleChains(
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) @Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@ -302,7 +295,7 @@ public class RuleChainController extends BaseController {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
RuleChainType type = RuleChainType.CORE; RuleChainType type = RuleChainType.CORE;
if (typeStr != null && typeStr.trim().length() > 0) { if (StringUtils.isNotBlank(typeStr)) {
type = RuleChainType.valueOf(typeStr); type = RuleChainType.valueOf(typeStr);
} }
return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink)); return checkNotNull(ruleChainService.findTenantRuleChainsByType(tenantId, type, pageLink));
@ -311,9 +304,9 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Delete rule chain (deleteRuleChain)", @ApiOperation(value = "Delete rule chain (deleteRuleChain)",
notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " + notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " +
"Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH) "Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @DeleteMapping("/ruleChain/{ruleChainId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public void deleteRuleChain( public void deleteRuleChain(
@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
@ -326,9 +319,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)", @ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)",
notes = "Gets the input message from the debug events for specified Rule Chain Id. " + notes = "Gets the input message from the debug events for specified Rule Chain Id. " +
"Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) "Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) @GetMapping("/ruleNode/{ruleNodeId}/debugIn")
@ResponseBody
public JsonNode getLatestRuleNodeDebugInput( public JsonNode getLatestRuleNodeDebugInput(
@Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION) @Parameter(description = RULE_NODE_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException {
@ -343,8 +335,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Is TBEL script executor enabled", @ApiOperation(value = "Is TBEL script executor enabled",
notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET) @GetMapping("/ruleChain/tbelEnabled")
@ResponseBody
public Boolean isTbelEnabled() { public Boolean isTbelEnabled() {
return tbelEnabled; return tbelEnabled;
} }
@ -352,13 +343,12 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Test Script function", @ApiOperation(value = "Test Script function",
notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) notes = TEST_SCRIPT_FUNCTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @PostMapping("/ruleChain/testScript")
@ResponseBody
public JsonNode testScript( public JsonNode testScript(
@Parameter(description = "Script language: JS or TBEL") @Parameter(description = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang, @RequestParam(required = false) ScriptLanguage scriptLang,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.") @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException, JsonProcessingException { @RequestBody JsonNode inputParams) {
String script = inputParams.get("script").asText(); String script = inputParams.get("script").asText();
String scriptType = inputParams.get("scriptType").asText(); String scriptType = inputParams.get("scriptType").asText();
JsonNode argNamesJson = inputParams.get("argNames"); JsonNode argNamesJson = inputParams.get("argNames");
@ -366,8 +356,7 @@ public class RuleChainController extends BaseController {
String data = inputParams.get("msg").asText(); String data = inputParams.get("msg").asText();
JsonNode metadataJson = inputParams.get("metadata"); JsonNode metadataJson = inputParams.get("metadata");
Map<String, String> metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<Map<String, String>>() { Map<String, String> metadata = JacksonUtil.convertValue(metadataJson, new TypeReference<>() {});
});
String msgType = inputParams.get("msgType").asText(); String msgType = inputParams.get("msgType").asText();
String output = ""; String output = "";
String errorText = ""; String errorText = "";
@ -384,55 +373,40 @@ public class RuleChainController extends BaseController {
} }
engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames); engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames);
} }
TbMsg inMsg = TbMsg.newMsg()
var inMsg = TbMsg.newMsg()
.type(msgType) .type(msgType)
.copyMetaData(new TbMsgMetaData(metadata)) .copyMetaData(new TbMsgMetaData(metadata))
.dataType(TbMsgDataType.JSON) .dataType(TbMsgDataType.JSON)
.data(data) .data(data)
.build(); .build();
switch (scriptType) {
case "update": output = switch (scriptType) {
output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); case "update" -> msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; case "generate" -> msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
case "generate": case "filter" -> Boolean.toString(engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); case "switch" -> JacksonUtil.toString(engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
break; case "json" -> JacksonUtil.toString(engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS));
case "filter": case "string" -> engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); default -> throw new IllegalArgumentException("Unsupported script type: " + scriptType);
output = Boolean.toString(result); };
break;
case "switch":
Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(states);
break;
case "json":
JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
break;
case "string":
output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS);
break;
default:
throw new IllegalArgumentException("Unsupported script type: " + scriptType);
}
} catch (Exception e) { } catch (Exception e) {
log.error("Error evaluating JS function", e); log.error("Error evaluating JS function", e);
errorText = e.getMessage(); Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally { } finally {
if (engine != null) { if (engine != null) {
engine.destroy(); engine.destroy();
} }
} }
ObjectNode result = JacksonUtil.newObjectNode(); return JacksonUtil.newObjectNode()
result.put("output", output); .put("output", output)
result.put("error", errorText); .put("error", errorText);
return result;
} }
@ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) @GetMapping(value = "/ruleChains/export", params = {"limit"})
@ResponseBody
public RuleChainData exportRuleChains( public RuleChainData exportRuleChains(
@Parameter(description = "A limit of rule chains to export.", required = true) @Parameter(description = "A limit of rule chains to export.", required = true)
@RequestParam("limit") int limit) throws ThingsboardException { @RequestParam("limit") int limit) throws ThingsboardException {
@ -443,8 +417,7 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) @PostMapping("/ruleChains/import")
@ResponseBody
public List<RuleChainImportResult> importRuleChains( public List<RuleChainImportResult> importRuleChains(
@Parameter(description = "A JSON value representing the rule chains.") @Parameter(description = "A JSON value representing the rule chains.")
@RequestBody RuleChainData ruleChainData, @RequestBody RuleChainData ruleChainData,
@ -454,12 +427,12 @@ public class RuleChainController extends BaseController {
return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration); return ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration);
} }
private String msgToOutput(TbMsg msg) throws Exception { private String msgToOutput(TbMsg msg) {
JsonNode resultNode = convertMsgToOut(msg); JsonNode resultNode = convertMsgToOut(msg);
return JacksonUtil.toString(resultNode); return JacksonUtil.toString(resultNode);
} }
private String msgToOutput(List<TbMsg> msgs) throws Exception { private String msgToOutput(List<TbMsg> msgs) {
JsonNode resultNode; JsonNode resultNode;
if (msgs.size() > 1) { if (msgs.size() > 1) {
resultNode = JacksonUtil.newArrayNode(); resultNode = JacksonUtil.newArrayNode();
@ -473,7 +446,7 @@ public class RuleChainController extends BaseController {
return JacksonUtil.toString(resultNode); return JacksonUtil.toString(resultNode);
} }
private JsonNode convertMsgToOut(TbMsg msg) throws Exception { private JsonNode convertMsgToOut(TbMsg msg) {
ObjectNode msgData = JacksonUtil.newObjectNode(); ObjectNode msgData = JacksonUtil.newObjectNode();
if (!StringUtils.isEmpty(msg.getData())) { if (!StringUtils.isEmpty(msg.getData())) {
msgData.set("msg", JacksonUtil.toJsonNode(msg.getData())); msgData.set("msg", JacksonUtil.toJsonNode(msg.getData()));
@ -492,8 +465,7 @@ public class RuleChainController extends BaseController {
"Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " + "Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " +
"\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH) "\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) @PostMapping("/edge/{edgeId}/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId, public RuleChain assignRuleChainToEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
@ -514,8 +486,7 @@ public class RuleChainController extends BaseController {
EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION +
"Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH) "Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @DeleteMapping("/edge/{edgeId}/ruleChain/{ruleChainId}")
@ResponseBody
public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId, public RuleChain unassignRuleChainFromEdge(@PathVariable("edgeId") String strEdgeId,
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId); checkParameter("edgeId", strEdgeId);
@ -530,9 +501,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)",
notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @GetMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"})
@ResponseBody
public PageData<RuleChain> getEdgeRuleChains( public PageData<RuleChain> getEdgeRuleChains(
@Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(EDGE_ID) String strEdgeId, @PathVariable(EDGE_ID) String strEdgeId,
@ -557,9 +527,8 @@ public class RuleChainController extends BaseController {
@ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)",
notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " + notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " +
"Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/edgeTemplateRoot")
@ResponseBody
public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain setEdgeTemplateRootRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -572,8 +541,7 @@ public class RuleChainController extends BaseController {
notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " + notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " +
"Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) @PostMapping("/ruleChain/{ruleChainId}/autoAssignToEdge")
@ResponseBody
public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain setAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -586,8 +554,7 @@ public class RuleChainController extends BaseController {
notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " + notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " +
"Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH) "Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) @DeleteMapping("/ruleChain/{ruleChainId}/autoAssignToEdge")
@ResponseBody
public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION) public RuleChain unsetAutoAssignToEdgeRuleChain(@Parameter(description = RULE_CHAIN_ID_PARAM_DESCRIPTION)
@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
checkParameter(RULE_CHAIN_ID, strRuleChainId); checkParameter(RULE_CHAIN_ID, strRuleChainId);
@ -599,9 +566,8 @@ public class RuleChainController extends BaseController {
// TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign // TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign
@ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)", @ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)",
notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) @GetMapping("/ruleChain/autoAssignToEdgeRuleChains")
@ResponseBody
public List<RuleChain> getAutoAssignToEdgeRuleChains() throws ThingsboardException { public List<RuleChain> getAutoAssignToEdgeRuleChains() throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId(); TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChain> result = new ArrayList<>(); List<RuleChain> result = new ArrayList<>();
@ -612,4 +578,5 @@ public class RuleChainController extends BaseController {
} }
return checkNotNull(result); return checkNotNull(result);
} }
} }

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

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

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

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

20
application/src/main/java/org/thingsboard/server/service/ai/AiChatModelService.java

@ -0,0 +1,20 @@
/**
* 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.ai;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
public interface AiChatModelService extends RuleEngineAiChatModelService {}

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

@ -0,0 +1,40 @@
/**
* 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.model.chat.ChatModel;
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.AiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
@Service
@RequiredArgsConstructor
class AiChatModelServiceImpl implements AiChatModelService {
private final Langchain4jChatModelConfigurer chatModelConfigurer;
private final AiRequestsExecutor aiRequestsExecutor;
@Override
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
return aiRequestsExecutor.sendChatRequestAsync(langChainChatModel, chatRequest);
}
}

27
application/src/main/java/org/thingsboard/server/service/ai/AiRequestsExecutor.java

@ -0,0 +1,27 @@
/**
* 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
public interface AiRequestsExecutor {
FluentFuture<ChatResponse> sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest);
}

86
application/src/main/java/org/thingsboard/server/service/ai/DefaultAiRequestsExecutor.java

@ -0,0 +1,86 @@
/**
* 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.ai;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import java.time.Duration;
import java.util.concurrent.Executors;
@Lazy
@Component
@RequiredArgsConstructor
class DefaultAiRequestsExecutor implements AiRequestsExecutor {
private final AiRequestsExecutorProperties properties;
@Data
@Validated
@Configuration
@ConfigurationProperties(prefix = "actors.rule.ai-requests-thread-pool")
private static class AiRequestsExecutorProperties {
@NotBlank(message = "Pool name must be not blank")
private String poolName = "ai-requests";
@Min(value = 1, message = "Pool size must be at least 1")
private int poolSize = 50;
@Min(value = 1, message = "Termination timeout must be at least 1 second")
private int terminationTimeoutSeconds = 60;
}
private ListeningExecutorService executorService;
@PostConstruct
private void init() {
executorService = MoreExecutors.listeningDecorator(
Executors.newFixedThreadPool(properties.getPoolSize(), ThingsBoardThreadFactory.forName(properties.getPoolName()))
);
}
@Override
public FluentFuture<ChatResponse> sendChatRequestAsync(ChatModel chatModel, ChatRequest chatRequest) {
return FluentFuture.from(executorService.submit(() -> chatModel.chat(chatRequest)));
}
@PreDestroy
private void destroy() {
if (executorService != null) {
MoreExecutors.shutdownAndAwaitTermination(executorService, Duration.ofSeconds(properties.getTerminationTimeoutSeconds()));
executorService = null;
}
}
}

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

@ -0,0 +1,269 @@
/**
* 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.ai;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.vertexai.Transport;
import com.google.cloud.vertexai.VertexAI;
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.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.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;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.time.Duration;
@Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@Override
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(AzureOpenAiChatModelConfig chatModelConfig) {
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
return AzureOpenAiChatModel.builder()
.endpoint(providerConfig.endpoint())
.serviceVersion(providerConfig.serviceVersion())
.apiKey(providerConfig.apiKey())
.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(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(GoogleVertexAiGeminiChatModelConfig chatModelConfig) {
GoogleVertexAiGeminiProviderConfig providerConfig = chatModelConfig.providerConfig();
// construct service account credentials using service account key JSON
ServiceAccountCredentials serviceAccountCredentials;
try {
serviceAccountCredentials = ServiceAccountCredentials.fromStream(new ByteArrayInputStream(providerConfig.serviceAccountKey().getBytes()));
} catch (IOException e) {
throw new RuntimeException("Failed to parse service account key JSON", e);
}
PredictionServiceSettings predictionServiceClientSettings;
try {
// create prediction service settings for REST transport with service account key credentials
PredictionServiceSettings.Builder settingsBuilder = PredictionServiceSettings.newHttpJsonBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(serviceAccountCredentials));
// get the retry settings that control request timeout for generateContent RPC
RetrySettings.Builder retrySettings = settingsBuilder
.generateContentSettings()
.getRetrySettings()
.toBuilder();
// set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
}
// set updated retry settings
settingsBuilder.generateContentSettings().setRetrySettings(retrySettings.build());
// build the client settings
predictionServiceClientSettings = settingsBuilder.build();
} catch (IOException e) {
throw new RuntimeException("Failed to create prediction service client settings", e);
}
// construct Vertex AI instance
var vertexAI = new VertexAI.Builder()
.setProjectId(providerConfig.projectId())
.setLocation(providerConfig.location())
.setPredictionClientSupplier(() -> createPredictionServiceClient(predictionServiceClientSettings))
.setTransport(Transport.REST) // GRPC also possible, but likely does not work with service account keys
.build();
// map model config to generation config
var generationConfigBuilder = GenerationConfig.newBuilder();
if (chatModelConfig.temperature() != null) {
generationConfigBuilder.setTemperature(chatModelConfig.temperature().floatValue());
}
if (chatModelConfig.topP() != null) {
generationConfigBuilder.setTopP(chatModelConfig.topP().floatValue());
}
if (chatModelConfig.topK() != null) {
generationConfigBuilder.setTopK(chatModelConfig.topK());
}
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setFrequencyPenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (chatModelConfig.frequencyPenalty() != null) {
generationConfigBuilder.setPresencePenalty(chatModelConfig.frequencyPenalty().floatValue());
}
if (chatModelConfig.maxOutputTokens() != null) {
generationConfigBuilder.setMaxOutputTokens(chatModelConfig.maxOutputTokens());
}
var generationConfig = generationConfigBuilder.build();
// construct generative model instance
var generativeModel = new GenerativeModel(chatModelConfig.modelId(), vertexAI).withGenerationConfig(generationConfig);
return new VertexAiGeminiChatModel(generativeModel, generationConfig, chatModelConfig.maxRetries());
}
private static PredictionServiceClient createPredictionServiceClient(PredictionServiceSettings settings) {
try {
return PredictionServiceClient.create(settings);
} catch (IOException e) {
throw new RuntimeException("Failed to create prediction service client", e);
}
}
@Override
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(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(AmazonBedrockChatModelConfig chatModelConfig) {
AmazonBedrockProviderConfig providerConfig = chatModelConfig.providerConfig();
var credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(providerConfig.accessKeyId(), providerConfig.secretAccessKey())
);
var bedrockClient = BedrockRuntimeClient.builder()
.region(Region.of(providerConfig.region()))
.credentialsProvider(credentialsProvider)
.build();
var defaultChatRequestParams = ChatRequestParameters.builder()
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.maxOutputTokens(chatModelConfig.maxOutputTokens())
.build();
return BedrockChatModel.builder()
.client(bedrockClient)
.modelId(chatModelConfig.modelId())
.defaultRequestParameters(defaultChatRequestParams)
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries())
.build();
}
@Override
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();
}
private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
}
}

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

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

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

@ -58,8 +58,7 @@ public class RelatedEdgesSourcingListener {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event); log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try { try {
switch (event.getActionType()) { switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e); log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
@ -67,7 +66,10 @@ public class RelatedEdgesSourcingListener {
}); });
} }
@TransactionalEventListener(fallbackExecution = true) @TransactionalEventListener(
fallbackExecution = true,
condition = "#event.entityId.getEntityType() != T(org.thingsboard.server.common.data.EntityType).AI_MODEL"
)
public void handleEvent(DeleteEntityEvent<?> event) { public void handleEvent(DeleteEntityEvent<?> event) {
executorService.submit(() -> { executorService.submit(() -> {
log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event); log.trace("[{}] DeleteEntityEvent called: {}", event.getTenantId(), event);

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

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

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

@ -97,7 +97,7 @@ public abstract class AbstractTbEntityService {
return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID);
} }
protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { protected ListenableFuture<UUID> autoCommit(User user, EntityId entityId) {
if (vcService != null) { if (vcService != null) {
return vcService.autoCommit(user, entityId); return vcService.autoCommit(user, entityId);
} else { } else {
@ -106,7 +106,7 @@ public abstract class AbstractTbEntityService {
} }
} }
protected ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception { protected ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) {
if (vcService != null) { if (vcService != null) {
return vcService.autoCommit(user, entityType, entityIds); return vcService.autoCommit(user, entityType, entityIds);
} else { } else {

80
application/src/main/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelService.java

@ -0,0 +1,80 @@
/**
* 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.entitiy.ai;
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.AiModel;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import static java.util.Objects.requireNonNullElseGet;
@Service
@TbCoreComponent
@RequiredArgsConstructor
class DefaultTbAiModelService extends AbstractTbEntityService implements TbAiModelService {
private final AiModelService aiModelService;
@Override
public AiModel save(AiModel model, User user) {
var actionType = model.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
var tenantId = user.getTenantId();
model.setTenantId(tenantId);
AiModel savedModel;
try {
savedModel = aiModelService.save(model);
autoCommit(user, savedModel.getId());
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, requireNonNullElseGet(model.getId(), () -> emptyId(EntityType.AI_MODEL)), model, actionType, user, e);
throw e;
}
logEntityActionService.logEntityAction(tenantId, savedModel.getId(), savedModel, actionType, user);
return savedModel;
}
@Override
public boolean delete(AiModel model, User user) {
var actionType = ActionType.DELETED;
var tenantId = user.getTenantId();
var modelId = model.getId();
boolean deleted;
try {
deleted = aiModelService.deleteByTenantIdAndId(tenantId, modelId);
} catch (Exception e) {
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, e, modelId.toString());
throw e;
}
if (deleted) {
logEntityActionService.logEntityAction(tenantId, modelId, model, actionType, user, modelId.toString());
}
return deleted;
}
}

27
application/src/main/java/org/thingsboard/server/service/entitiy/ai/TbAiModelService.java

@ -0,0 +1,27 @@
/**
* 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.entitiy.ai;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
public interface TbAiModelService {
AiModel save(AiModel model, User user);
boolean delete(AiModel model, User user);
}

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService {
@Autowired @Autowired
private AlarmCommentService alarmCommentService; private AlarmCommentService alarmCommentService;
@ -68,4 +68,5 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem
throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
} }
} }

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

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

43
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java

@ -43,33 +43,30 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor<Alarms
EntityType entityType = entityId.getEntityType(); EntityType entityType = entityId.getEntityType();
TenantId tenantId = task.getTenantId(); TenantId tenantId = task.getTenantId();
if (entityType == EntityType.DEVICE || entityType == EntityType.ASSET) { if (task.getAlarms() == null) {
if (task.getAlarms() == null) { AlarmId lastId = null;
AlarmId lastId = null; long lastCreatedTime = 0;
long lastCreatedTime = 0; while (true) {
while (true) { List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128);
List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); if (alarms.isEmpty()) {
if (alarms.isEmpty()) { break;
break; }
}
housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList()));
TbPair<UUID, Long> last = alarms.get(alarms.size() - 1); TbPair<UUID, Long> last = alarms.get(alarms.size() - 1);
lastId = new AlarmId(last.getFirst()); lastId = new AlarmId(last.getFirst());
lastCreatedTime = last.getSecond(); lastCreatedTime = last.getSecond();
log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size());
}
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} }
@Override @Override

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

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

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

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

183
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -22,9 +22,9 @@ import freemarker.template.Template;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -64,55 +64,37 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@Service
@Slf4j @Slf4j
@Service
@RequiredArgsConstructor
public class DefaultMailService implements MailService { public class DefaultMailService implements MailService {
public static final String TARGET_EMAIL = "targetEmail"; private static final String TARGET_EMAIL = "targetEmail";
public static final String UTF_8 = "UTF-8"; private static final String UTF_8 = "UTF-8";
private static final long DEFAULT_TIMEOUT = 10_000;
private final ScheduledExecutorService timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog");
private final MessageSource messages; private final MessageSource messages;
private final Configuration freemarkerConfig; private final Configuration freemarkerConfig;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private static final long DEFAULT_TIMEOUT = 10_000;
@Lazy @Lazy
@Autowired private final TbApiUsageStateService apiUsageStateService;
private TbApiUsageStateService apiUsageStateService; private final MailSenderInternalExecutorService mailExecutorService;
private final PasswordResetExecutorService passwordResetExecutorService;
@Autowired private final TbMailContextComponent ctx;
private MailSenderInternalExecutorService mailExecutorService; private final RateLimitService rateLimitService;
@Autowired
private PasswordResetExecutorService passwordResetExecutorService;
@Autowired
private TbMailContextComponent ctx;
@Autowired
private RateLimitService rateLimitService;
@Value("${mail.per_tenant_rate_limits:}") @Value("${mail.per_tenant_rate_limits:}")
private String perTenantRateLimitConfig; private String perTenantRateLimitConfig;
private final ScheduledExecutorService timeoutScheduler;
private TbMailSender mailSender; private TbMailSender mailSender;
private String mailFrom; private String mailFrom;
private long timeout; private long timeout;
public DefaultMailService(MessageSource messages, Configuration freemarkerConfig, AdminSettingsService adminSettingsService, TbApiUsageReportClient apiUsageClient) {
this.messages = messages;
this.freemarkerConfig = freemarkerConfig;
this.adminSettingsService = adminSettingsService;
this.apiUsageClient = apiUsageClient;
this.timeoutScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("mail-service-watchdog");
}
@PostConstruct @PostConstruct
private void init() { private void init() {
updateMailConfiguration(); updateMailConfiguration();
@ -120,9 +102,7 @@ public class DefaultMailService implements MailService {
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if (timeoutScheduler != null) { timeoutScheduler.shutdownNow();
timeoutScheduler.shutdownNow();
}
} }
@Override @Override
@ -311,22 +291,21 @@ public class DefaultMailService implements MailService {
model.put("apiFeature", apiFeature.getLabel()); model.put("apiFeature", apiFeature.getLabel());
model.put(TARGET_EMAIL, email); model.put(TARGET_EMAIL, email);
String message = null; String message = switch (stateValue) {
case ENABLED -> {
switch (stateValue) {
case ENABLED:
model.put("apiLabel", toEnabledValueLabel(apiFeature)); model.put("apiLabel", toEnabledValueLabel(apiFeature));
message = mergeTemplateIntoString("state.enabled.ftl", model); yield mergeTemplateIntoString("state.enabled.ftl", model);
break; }
case WARNING: case WARNING -> {
model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState)); model.put("apiValueLabel", toDisabledValueLabel(apiFeature) + " " + toWarningValueLabel(recordState));
message = mergeTemplateIntoString("state.warning.ftl", model); yield mergeTemplateIntoString("state.warning.ftl", model);
break; }
case DISABLED: case DISABLED -> {
model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState)); model.put("apiLimitValueLabel", toDisabledValueLabel(apiFeature) + " " + toDisabledValueLabel(recordState));
message = mergeTemplateIntoString("state.disabled.ftl", model); yield mergeTemplateIntoString("state.disabled.ftl", model);
break; }
} };
sendMail(mailSender, mailFrom, email, subject, message, timeout); sendMail(mailSender, mailFrom, email, subject, message, timeout);
} }
@ -341,89 +320,55 @@ public class DefaultMailService implements MailService {
} }
private String toEnabledValueLabel(ApiFeature apiFeature) { private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { return switch (apiFeature) {
case DB: case DB -> "save";
return "save"; case TRANSPORT -> "receive";
case TRANSPORT: case JS -> "invoke";
return "receive"; case RE -> "process";
case JS: case EMAIL, SMS -> "send";
return "invoke"; case ALARM -> "create";
case RE: default -> throw new RuntimeException("Not implemented!");
return "process"; };
case EMAIL:
case SMS:
return "send";
case ALARM:
return "create";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toDisabledValueLabel(ApiFeature apiFeature) { private String toDisabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { return switch (apiFeature) {
case DB: case DB -> "saved";
return "saved"; case TRANSPORT -> "received";
case TRANSPORT: case JS -> "invoked";
return "received"; case RE -> "processed";
case JS: case EMAIL, SMS -> "sent";
return "invoked"; case ALARM -> "created";
case RE: default -> throw new RuntimeException("Not implemented!");
return "processed"; };
case EMAIL:
case SMS:
return "sent";
case ALARM:
return "created";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toWarningValueLabel(ApiUsageRecordState recordState) { private String toWarningValueLabel(ApiUsageRecordState recordState) {
String valueInM = recordState.getValueAsString(); String valueInM = recordState.getValueAsString();
String thresholdInM = recordState.getThresholdAsString(); String thresholdInM = recordState.getThresholdAsString();
switch (recordState.getKey()) { return switch (recordState.getKey()) {
case STORAGE_DP_COUNT: case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> valueInM + " out of " + thresholdInM + " allowed data points";
case TRANSPORT_DP_COUNT: case TRANSPORT_MSG_COUNT -> valueInM + " out of " + thresholdInM + " allowed messages";
return valueInM + " out of " + thresholdInM + " allowed data points"; case JS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed JavaScript functions";
case TRANSPORT_MSG_COUNT: case TBEL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Tbel functions";
return valueInM + " out of " + thresholdInM + " allowed messages"; case RE_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
case JS_EXEC_COUNT: case EMAIL_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed Email messages";
return valueInM + " out of " + thresholdInM + " allowed JavaScript functions"; case SMS_EXEC_COUNT -> valueInM + " out of " + thresholdInM + " allowed SMS messages";
case TBEL_EXEC_COUNT: default -> throw new RuntimeException("Not implemented!");
return valueInM + " out of " + thresholdInM + " allowed Tbel functions"; };
case RE_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Rule Engine messages";
case EMAIL_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed Email messages";
case SMS_EXEC_COUNT:
return valueInM + " out of " + thresholdInM + " allowed SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
} }
private String toDisabledValueLabel(ApiUsageRecordState recordState) { private String toDisabledValueLabel(ApiUsageRecordState recordState) {
switch (recordState.getKey()) { return switch (recordState.getKey()) {
case STORAGE_DP_COUNT: case STORAGE_DP_COUNT, TRANSPORT_DP_COUNT -> recordState.getValueAsString() + " data points";
case TRANSPORT_DP_COUNT: case TRANSPORT_MSG_COUNT -> recordState.getValueAsString() + " messages";
return recordState.getValueAsString() + " data points"; case JS_EXEC_COUNT -> "JavaScript functions " + recordState.getValueAsString() + " times";
case TRANSPORT_MSG_COUNT: case TBEL_EXEC_COUNT -> "TBEL functions " + recordState.getValueAsString() + " times";
return recordState.getValueAsString() + " messages"; case RE_EXEC_COUNT -> recordState.getValueAsString() + " Rule Engine messages";
case JS_EXEC_COUNT: case EMAIL_EXEC_COUNT -> recordState.getValueAsString() + " Email messages";
return "JavaScript functions " + recordState.getValueAsString() + " times"; case SMS_EXEC_COUNT -> recordState.getValueAsString() + " SMS messages";
case TBEL_EXEC_COUNT: default -> throw new RuntimeException("Not implemented!");
return "TBEL functions " + recordState.getValueAsString() + " times"; };
case RE_EXEC_COUNT:
return recordState.getValueAsString() + " Rule Engine messages";
case EMAIL_EXEC_COUNT:
return recordState.getValueAsString() + " Email messages";
case SMS_EXEC_COUNT:
return recordState.getValueAsString() + " SMS messages";
default:
throw new RuntimeException("Not implemented!");
}
} }
private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email, private void sendMail(JavaMailSenderImpl mailSender, String mailFrom, String email,

22
application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java

@ -25,6 +25,7 @@ import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.gson.GsonFactory;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.mail.MailException; import org.springframework.mail.MailException;
@ -50,8 +51,10 @@ public class TbMailSender extends JavaMailSenderImpl {
private final TbMailContextComponent ctx; private final TbMailContextComponent ctx;
private final Lock lock; private final Lock lock;
@Getter
private final Boolean oauth2Enabled; private final Boolean oauth2Enabled;
private volatile String accessToken; private volatile String accessToken;
@Getter
private volatile long tokenExpires; private volatile long tokenExpires;
public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) { public TbMailSender(TbMailContextComponent ctx, JsonNode jsonConfig) {
@ -70,14 +73,6 @@ public class TbMailSender extends JavaMailSenderImpl {
setJavaMailProperties(createJavaMailProperties(jsonConfig)); setJavaMailProperties(createJavaMailProperties(jsonConfig));
} }
public Boolean getOauth2Enabled() {
return oauth2Enabled;
}
public long getTokenExpires() {
return tokenExpires;
}
@Override @Override
protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
updateOauth2PasswordIfExpired(); updateOauth2PasswordIfExpired();
@ -98,8 +93,8 @@ public class TbMailSender extends JavaMailSenderImpl {
super.testConnection(); super.testConnection();
} }
public void updateOauth2PasswordIfExpired() { public void updateOauth2PasswordIfExpired() {
if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())){ if (getOauth2Enabled() && (System.currentTimeMillis() > getTokenExpires())) {
refreshAccessToken(); refreshAccessToken();
setPassword(accessToken); setPassword(accessToken);
} }
@ -168,8 +163,8 @@ public class TbMailSender extends JavaMailSenderImpl {
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret)) .setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.execute(); .execute();
if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) { if (MailOauth2Provider.OFFICE_365.name().equals(providerId)) {
((ObjectNode)jsonValue).put("refreshToken", tokenResponse.getRefreshToken()); ((ObjectNode) jsonValue).put("refreshToken", tokenResponse.getRefreshToken());
((ObjectNode)jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli()); ((ObjectNode) jsonValue).put("refreshTokenExpires", Instant.now().plus(Duration.ofDays(AZURE_DEFAULT_REFRESH_TOKEN_LIFETIME_IN_DAYS)).toEpochMilli());
ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings); ctx.getAdminSettingsService().saveAdminSettings(TenantId.SYS_TENANT_ID, settings);
} }
accessToken = tokenResponse.getAccessToken(); accessToken = tokenResponse.getAccessToken();
@ -190,4 +185,5 @@ public class TbMailSender extends JavaMailSenderImpl {
throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort)); throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
} }
} }
}
}

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

@ -208,9 +208,26 @@ public class DefaultEntityQueryService implements EntityQueryService {
@Override @Override
public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) { public long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query) {
if (query.getEntityFilter() != null) {
EntityDataQuery entityDataQuery = this.buildEntityDataQuery(query);
PageData<EntityData> entities = entityService.findEntityDataByQuery(securityUser.getTenantId(),
securityUser.getCustomerId(), entityDataQuery);
if (entities.getTotalElements() > 0) {
List<EntityId> entityIds = entities.getData().stream().map(EntityData::getEntityId).toList();
return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query, entityIds);
} else {
return 0;
}
}
return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query); return alarmService.countAlarmsByQuery(securityUser.getTenantId(), securityUser.getCustomerId(), query);
} }
private EntityDataQuery buildEntityDataQuery(AlarmCountQuery query) {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)));
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
}
private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) { private EntityDataQuery buildEntityDataQuery(AlarmDataQuery query) {
EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder(); EntityDataSortOrder sortOrder = query.getPageLink().getSortOrder();
EntityDataSortOrder entitiesSortOrder; EntityDataSortOrder entitiesSortOrder;
@ -220,7 +237,7 @@ public class DefaultEntityQueryService implements EntityQueryService {
entitiesSortOrder = sortOrder; entitiesSortOrder = sortOrder;
} }
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
} }
@Override @Override

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

@ -583,17 +583,18 @@ public class DefaultTbClusterService implements TbClusterService {
TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer(); TbQueueProducer<TbProtoQueueMsg<ToRuleEngineNotificationMsg>> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE); Set<String> tbRuleEngineServices = partitionService.getAllServiceIds(ServiceType.TB_RULE_ENGINE);
EntityType entityType = msg.getEntityId().getEntityType(); EntityType entityType = msg.getEntityId().getEntityType();
if (entityType.equals(EntityType.TENANT) if (entityType.isOneOf(
|| entityType.equals(EntityType.TENANT_PROFILE) EntityType.TENANT,
|| entityType.equals(EntityType.DEVICE_PROFILE) EntityType.API_USAGE_STATE,
|| (entityType.equals(EntityType.ASSET) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) EntityType.ENTITY_VIEW,
|| entityType.equals(EntityType.ASSET_PROFILE) EntityType.NOTIFICATION_RULE,
|| entityType.equals(EntityType.API_USAGE_STATE) EntityType.CALCULATED_FIELD,
|| (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) EntityType.TENANT_PROFILE,
|| entityType.equals(EntityType.ENTITY_VIEW) EntityType.DEVICE_PROFILE,
|| entityType.equals(EntityType.NOTIFICATION_RULE) EntityType.ASSET_PROFILE,
|| entityType.equals(EntityType.CALCULATED_FIELD) EntityType.JOB)
|| entityType.equals(EntityType.JOB) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
) { ) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);

3
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -121,7 +121,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
errorPrefix = "/login?loginError="; errorPrefix = "/login?loginError=";
} }
getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix + getRedirectStrategy().sendRedirect(request, response, baseUrl + errorPrefix +
URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8));
} }
} }
@ -138,4 +138,5 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
} }
return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken(); return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken();
} }
} }

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

@ -21,7 +21,8 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
public enum Resource { public enum Resource {
ADMIN_SETTINGS(),
ADMIN_SETTINGS(EntityType.ADMIN_SETTINGS),
ALARM(EntityType.ALARM), ALARM(EntityType.ALARM),
DEVICE(EntityType.DEVICE), DEVICE(EntityType.DEVICE),
ASSET(EntityType.ASSET), ASSET(EntityType.ASSET),
@ -51,7 +52,8 @@ public enum Resource {
NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE,
EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE),
MOBILE_APP_SETTINGS, MOBILE_APP_SETTINGS,
JOB(EntityType.JOB); JOB(EntityType.JOB),
AI_MODEL(EntityType.AI_MODEL);
private final Set<EntityType> entityTypes; private final Set<EntityType> entityTypes;
@ -75,4 +77,5 @@ public enum Resource {
} }
throw new IllegalArgumentException("Unknown EntityType: " + entityType.name()); throw new IllegalArgumentException("Unknown EntityType: " + entityType.name());
} }
} }

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

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

16
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java

@ -31,16 +31,12 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory {
@Override @Override
public SmsSender createSmsSender(SmsProviderConfiguration config) { public SmsSender createSmsSender(SmsProviderConfiguration config) {
switch (config.getType()) { return switch (config.getType()) {
case AWS_SNS: case AWS_SNS -> new AwsSmsSender((AwsSnsSmsProviderConfiguration) config);
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config); case TWILIO -> new TwilioSmsSender((TwilioSmsProviderConfiguration) config);
case TWILIO: case SMPP -> new SmppSmsSender((SmppSmsProviderConfiguration) config);
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config); default -> throw new RuntimeException("Unknown SMS provider type " + config.getType());
case SMPP: };
return new SmppSmsSender((SmppSmsProviderConfiguration) config);
default:
throw new RuntimeException("Unknown SMS provider type " + config.getType());
}
} }
} }

12
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.sms;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.NestedRuntimeException; import org.springframework.core.NestedRuntimeException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -37,8 +38,9 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
@Service
@Slf4j @Slf4j
@Service
@RequiredArgsConstructor
public class DefaultSmsService implements SmsService { public class DefaultSmsService implements SmsService {
private final SmsSenderFactory smsSenderFactory; private final SmsSenderFactory smsSenderFactory;
@ -48,13 +50,6 @@ public class DefaultSmsService implements SmsService {
private SmsSender smsSender; private SmsSender smsSender;
public DefaultSmsService(SmsSenderFactory smsSenderFactory, AdminSettingsService adminSettingsService, TbApiUsageStateService apiUsageStateService, TbApiUsageReportClient apiUsageClient) {
this.smsSenderFactory = smsSenderFactory;
this.adminSettingsService = adminSettingsService;
this.apiUsageStateService = apiUsageStateService;
this.apiUsageClient = apiUsageClient;
}
@PostConstruct @PostConstruct
private void init() { private void init() {
updateSmsConfiguration(); updateSmsConfiguration();
@ -148,4 +143,5 @@ public class DefaultSmsService implements SmsService {
return new ThingsboardException(String.format("Unable to send SMS: %s", message), return new ThingsboardException(String.format("Unable to send SMS: %s", message),
ThingsboardErrorCode.GENERAL); ThingsboardErrorCode.GENERAL);
} }
} }

12
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractEntityQuerySubCtx.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.DynamicValueSourceType;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.data.query.FilterPredicateType; import org.thingsboard.server.common.data.query.FilterPredicateType;
import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.KeyFilterPredicate; import org.thingsboard.server.common.data.query.KeyFilterPredicate;
@ -94,9 +95,14 @@ public abstract class TbAbstractEntityQuerySubCtx<T extends EntityCountQuery> ex
public void setAndResolveQuery(T query) { public void setAndResolveQuery(T query) {
dynamicValues.clear(); dynamicValues.clear();
this.query = query; this.query = query;
if (query != null && query.getKeyFilters() != null) { if (query != null) {
for (KeyFilter filter : query.getKeyFilters()) { if (query.getEntityFilter() != null) {
registerDynamicValues(filter.getPredicate()); EntityFilter.resolveEntityFilter(query.getEntityFilter(), getTenantId(), getUserId(), getOwnerId());
}
if (query.getKeyFilters() != null) {
for (KeyFilter filter : query.getKeyFilters()) {
registerDynamicValues(filter.getPredicate());
}
} }
} }
resolve(getTenantId(), getCustomerId(), getUserId()); resolve(getTenantId(), getCustomerId(), getUserId());

5
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -107,6 +107,11 @@ public abstract class TbAbstractSubCtx {
return sessionRef.getSecurityCtx().getId(); return sessionRef.getSecurityCtx().getId();
} }
public EntityId getOwnerId() {
var customerId = getCustomerId();
return customerId != null && !customerId.isNullUid() ? customerId : getTenantId();
}
public void sendWsMsg(CmdUpdate update) { public void sendWsMsg(CmdUpdate update) {
wsLock.lock(); wsLock.lock();
try { try {

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

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

36
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AiModelExportService.java

@ -0,0 +1,36 @@
/**
* 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 org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
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;
import java.util.Set;
@Service
@TbCoreComponent
class AiModelExportService extends BaseEntityExportService<AiModelId, AiModel, EntityExportData<AiModel>> {
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.AI_MODEL);
}
}

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

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

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

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

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

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageExportService extends BaseEntityExportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, OtaPackage otaPackage, OtaPackageExportData exportData) {
otaPackage.setDeviceProfileId(getExternalIdOrElseInternal(ctx, otaPackage.getDeviceProfileId()));
}
@Override
protected OtaPackageExportData newExportData() {
return new OtaPackageExportData();
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.OTA_PACKAGE);
}
}

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

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

77
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AiModelImportService.java

@ -0,0 +1,77 @@
/**
* 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.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.AiModelService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
class AiModelImportService extends BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>> {
private final AiModelService aiModelService;
@Override
protected void setOwner(
TenantId tenantId,
AiModel model,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
model.setTenantId(tenantId);
}
@Override
protected AiModel prepare(
EntitiesImportCtx ctx,
AiModel model,
AiModel oldModel,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider
) {
return model;
}
@Override
protected AiModel deepCopy(AiModel model) {
return new AiModel(model);
}
@Override
protected AiModel saveOrUpdate(
EntitiesImportCtx ctx,
AiModel model,
EntityExportData<AiModel> exportData,
BaseEntityImportService<AiModelId, AiModel, EntityExportData<AiModel>>.IdProvider idProvider,
CompareResult compareResult
) {
return aiModelService.save(model);
}
@Override
public EntityType getEntityType() {
return EntityType.AI_MODEL;
}
}

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

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

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

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

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

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

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

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.id.OtaPackageId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.OtaPackageExportData;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class OtaPackageImportService extends BaseEntityImportService<OtaPackageId, OtaPackage, OtaPackageExportData> {
private final OtaPackageService otaPackageService;
@Override
protected void setOwner(TenantId tenantId, OtaPackage otaPackage, IdProvider idProvider) {
otaPackage.setTenantId(tenantId);
}
@Override
protected OtaPackage prepare(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackage oldOtaPackage, OtaPackageExportData exportData, IdProvider idProvider) {
otaPackage.setDeviceProfileId(idProvider.getInternalId(otaPackage.getDeviceProfileId()));
return otaPackage;
}
@Override
protected OtaPackage findExistingEntity(EntitiesImportCtx ctx, OtaPackage otaPackage, IdProvider idProvider) {
OtaPackage existingOtaPackage = super.findExistingEntity(ctx, otaPackage, idProvider);
if (existingOtaPackage == null && ctx.isFindExistingByName()) {
existingOtaPackage = otaPackageService.findOtaPackageByTenantIdAndTitleAndVersion(ctx.getTenantId(), otaPackage.getTitle(), otaPackage.getVersion());
}
return existingOtaPackage;
}
@Override
protected OtaPackage deepCopy(OtaPackage otaPackage) {
return new OtaPackage(otaPackage);
}
@Override
protected OtaPackage saveOrUpdate(EntitiesImportCtx ctx, OtaPackage otaPackage, OtaPackageExportData exportData, IdProvider idProvider, CompareResult compareResult) {
if (otaPackage.hasUrl()) {
OtaPackageInfo info = new OtaPackageInfo(otaPackage);
return new OtaPackage(otaPackageService.saveOtaPackageInfo(info, info.hasUrl()));
}
return otaPackageService.saveOtaPackage(otaPackage);
}
@Override
public EntityType getEntityType() {
return EntityType.OTA_PACKAGE;
}
}

7
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -114,9 +114,8 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
private final TbTransactionalCache<UUID, VersionControlTaskCacheEntry> taskCache; private final TbTransactionalCache<UUID, VersionControlTaskCacheEntry> taskCache;
private final VersionControlExecutor executor; private final VersionControlExecutor executor;
@SuppressWarnings("UnstableApiUsage")
@Override @Override
public ListenableFuture<UUID> saveEntitiesVersion(User user, VersionCreateRequest request) throws Exception { public ListenableFuture<UUID> saveEntitiesVersion(User user, VersionCreateRequest request) {
checkBranchName(request.getBranch()); checkBranchName(request.getBranch());
var pendingCommit = gitServiceQueue.prepareCommit(user, request); var pendingCommit = gitServiceQueue.prepareCommit(user, request);
DonAsynchron.withCallback(pendingCommit, commit -> { DonAsynchron.withCallback(pendingCommit, commit -> {
@ -546,7 +545,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityId entityId) {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null || repositorySettings.isReadOnly()) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
@ -573,7 +572,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception { public ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null || repositorySettings.isReadOnly()) { if (repositorySettings == null || repositorySettings.isReadOnly()) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);

4
application/src/main/java/org/thingsboard/server/service/sync/vc/EntitiesVersionControlService.java

@ -69,9 +69,9 @@ public interface EntitiesVersionControlService {
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception; ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception;
ListenableFuture<UUID> autoCommit(User user, EntityId entityId) throws Exception; ListenableFuture<UUID> autoCommit(User user, EntityId entityId);
ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds) throws Exception; ListenableFuture<UUID> autoCommit(User user, EntityType entityType, List<UUID> entityIds);
ListenableFuture<EntityDataInfo> getEntityDataInfo(User user, EntityId entityId, String versionId); ListenableFuture<EntityDataInfo> getEntityDataInfo(User user, EntityId entityId, String versionId);

6
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.telemetry; package org.thingsboard.server.service.telemetry;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -171,6 +172,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type);
} }
@Override
public FluentFuture<Alarm> findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestActiveByOriginatorAndTypeAsync(tenantId, originator, type);
}
@Override @Override
public Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { public Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type);

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

@ -208,7 +208,7 @@ ui:
# Help parameters # Help parameters
help: help:
# Base URL for UI help assets # Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.1}" base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.2}"
# Database telemetry parameters # Database telemetry parameters
database: database:
@ -323,7 +323,7 @@ cassandra:
poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}"
# Interval in milliseconds for printing Cassandra query queue statistic # Interval in milliseconds for printing Cassandra query queue statistic
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data type values except target to null for the same ts on save # When saving a value, set other data types to null (to avoid having multiple telemetry values with the same timestamp).
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}" set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}"
# log one of cassandra queries with specified frequency (0 - logging is disabled) # log one of cassandra queries with specified frequency (0 - logging is disabled)
print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
@ -464,6 +464,14 @@ actors:
allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}" allow_system_sms_service: "${ACTORS_RULE_ALLOW_SYSTEM_SMS_SERVICE:true}"
# Specify thread pool size for external call service # Specify thread pool size for external call service
external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}" external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:50}"
# Configuration for the thread pool that executes HTTP calls to AI provider APIs
ai-requests-thread-pool:
# The base name for threads
pool-name: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_NAME:ai-requests}"
# The maximum number of concurrent HTTP requests
pool-size: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_SIZE:50}"
# The maximum time in seconds to wait for active tasks to complete during graceful shutdown
termination-timeout-seconds: "${ACTORS_RULE_AI_REQUESTS_THREAD_POOL_TERMINATION_TIMEOUT_SECONDS:60}"
chain: chain:
# Errors for particular actors are persisted once per specified amount of milliseconds # Errors for particular actors are persisted once per specified amount of milliseconds
error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
@ -648,6 +656,9 @@ cache:
trendzSettings: trendzSettings:
timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL timeToLiveInMinutes: "${CACHE_SPECS_TRENDZ_SETTINGS_TTL:1440}" # Trendz settings cache TTL
maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled maxSize: "${CACHE_SPECS_TRENDZ_SETTINGS_MAX_SIZE:10000}" # 0 means the cache is disabled
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 # Deliberately placed outside the 'specs' group above
notificationRules: notificationRules:
@ -847,22 +858,23 @@ audit-log:
# Allowed values: OFF (disable), W (log write operations), RW (log read and write operations) # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations)
logging-level: logging-level:
mask: mask:
"device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "device": "${AUDIT_LOG_MASK_DEVICE:W}" # Device logging levels.
"asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "asset": "${AUDIT_LOG_MASK_ASSET:W}" # Asset logging levels.
"dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" # Dashboard logging levels.
"widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "widget_type": "${AUDIT_LOG_MASK_WIDGET_TYPE:W}" # Widget type logging levels.
"widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "widgets_bundle": "${AUDIT_LOG_MASK_WIDGETS_BUNDLE:W}" # Widget bundles logging levels.
"customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" # Customer logging levels.
"user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "user": "${AUDIT_LOG_MASK_USER:W}" # User logging levels.
"rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}" # Rule chain logging levels.
"alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "alarm": "${AUDIT_LOG_MASK_ALARM:W}" # Alarm logging levels.
"entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}" # Entity view logging levels.
"device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}" # Device profile logging levels.
"asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}" # Asset profile logging levels.
"edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "edge": "${AUDIT_LOG_MASK_EDGE:W}" # Edge logging levels.
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}" # TB resource logging levels.
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}" # Ota package logging levels.
"calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels. Allowed values: OFF (disable), W (log write operations), RW (log read and write operation "calculated_field": "${AUDIT_LOG_MASK_CALCULATED_FIELD:W}" # Calculated field logging levels.
"ai_model": "${AUDIT_LOG_MASK_AI_MODEL:W}" # AI model logging levels.
sink: sink:
# Type of external sink. possible options: none, elasticsearch # Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}" type: "${AUDIT_LOG_SINK_TYPE:none}"
@ -1661,7 +1673,7 @@ queue:
# Kafka properties for Notifications topics # Kafka properties for Notifications topics
notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
# Kafka properties for JS Executor topics # Kafka properties for JS Executor topics
js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:104857600;partitions:100;min.insync.replicas:1}" js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:86400000;segment.bytes:52428800;retention.bytes:104857600;partitions:30;min.insync.replicas:1}"
# Kafka properties for OTA updates topic # Kafka properties for OTA updates topic
ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" ota-updates: "${TB_QUEUE_KAFKA_OTA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}"
# Kafka properties for Version Control topic # Kafka properties for Version Control topic

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

@ -0,0 +1,615 @@
/**
* 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 com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.Test;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AnthropicProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.dao.service.DaoSqlTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AiModelControllerTest extends AbstractControllerTest {
/* --- Save API tests --- */
@Test
public void saveAiModel_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception {
// GIVEN
loginSysAdmin();
AiModel model = constructValidOpenAiModel("Test model");
// WHEN
ResultActions result = doPost("/api/ai/model", model);
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void saveAiModel_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception {
// GIVEN
loginCustomerUser();
AiModel model = constructValidOpenAiModel("Test model");
// WHEN
ResultActions result = doPost("/api/ai/model", model);
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void saveAiModel_whenCreatingValidModelAsTenantAdmin_shouldSucceed() throws Exception {
// GIVEN
loginTenantAdmin();
AiModel model = constructValidOpenAiModel("Test model");
// WHEN
var savedModel = doPost("/api/ai/model", model, AiModel.class);
// THEN
// verify returned object
assertThat(savedModel.getId()).isNotNull();
assertThat(savedModel.getUuidId()).isNotNull().isNotEqualTo(EntityId.NULL_UUID);
assertThat(savedModel.getId().getEntityType()).isEqualTo(EntityType.AI_MODEL);
assertThat(savedModel.getCreatedTime()).isPositive();
assertThat(savedModel.getVersion()).isEqualTo(1);
assertThat(savedModel.getTenantId()).isEqualTo(tenantId);
assertThat(savedModel.getName()).isEqualTo("Test model");
assertThat(savedModel.getConfiguration()).isEqualTo(model.getConfiguration());
assertThat(savedModel.getExternalId()).isNull();
}
@Test
public void saveAiModel_whenUpdatingExistingModelAsTenantAdmin_shouldSucceed() throws Exception {
// GIVEN
loginTenantAdmin();
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
var newModelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key-updated"))
.modelId("o4-mini")
.temperature(0.2)
.topP(0.4)
.frequencyPenalty(0.2)
.presencePenalty(0.5)
.maxOutputTokens(2000)
.timeoutSeconds(20)
.maxRetries(0)
.build();
model.setName("Test model updated");
model.setConfiguration(newModelConfig);
// WHEN
var updatedModel = doPost("/api/ai/model", model, AiModel.class);
// THEN
// verify returned object
assertThat(updatedModel.getId()).isEqualTo(model.getId());
assertThat(updatedModel.getCreatedTime()).isEqualTo(model.getCreatedTime());
assertThat(updatedModel.getVersion()).isEqualTo(2);
assertThat(updatedModel.getTenantId()).isEqualTo(tenantId);
assertThat(updatedModel.getName()).isEqualTo("Test model updated");
assertThat(updatedModel.getConfiguration()).isEqualTo(newModelConfig);
assertThat(updatedModel.getExternalId()).isNull();
}
/* --- Get by ID API tests --- */
@Test
public void getAiModelById_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception {
// GIVEN
loginSysAdmin();
// WHEN
ResultActions result = doGet("/api/ai/model/" + Uuids.timeBased());
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void getAiModelById_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception {
// GIVEN
loginCustomerUser();
// WHEN
ResultActions result = doGet("/api/ai/model/" + Uuids.timeBased());
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void getAiModelById_whenGettingExistingModelAsTenantAdmin_shouldReturnModel() throws Exception {
// GIVEN
loginTenantAdmin();
var saved = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
// WHEN
AiModel actual = doGet("/api/ai/model/" + saved.getId(), AiModel.class);
// THEN
assertThat(actual).isEqualTo(saved);
}
@Test
public void getAiModelById_whenGettingNonexistentModelAsTenantAdmin_shouldReturnNotFound() throws Exception {
// GIVEN
loginTenantAdmin();
var nonexistentModelId = new AiModelId(Uuids.timeBased());
// WHEN
ResultActions result = doGet("/api/ai/model/" + nonexistentModelId);
// THEN
result.andExpect(status().isNotFound())
.andExpect(statusReason(is("AI model with id [" + nonexistentModelId + "] is not found")));
}
/* --- Get paged API tests --- */
@Test
public void getAiModels_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception {
// GIVEN
loginSysAdmin();
// WHEN
ResultActions result = doGet("/api/ai/model?pageSize=10&page=0");
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void getAiModels_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception {
// GIVEN
loginCustomerUser();
// WHEN
ResultActions result = doGet("/api/ai/model?pageSize=10&page=0");
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void getAiModels_testPagination() throws Exception {
// GIVEN
loginTenantAdmin();
var model1 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 1"), AiModel.class);
var model2 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 2"), AiModel.class);
var model3 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 3"), AiModel.class);
var model4 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 4"), AiModel.class);
var model5 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 5"), AiModel.class);
// WHEN
PageData<AiModel> result = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(2, 1));
// THEN
assertThat(result.getData()).containsExactly(model3, model4);
assertThat(result.getTotalPages()).isEqualTo(3);
assertThat(result.getTotalElements()).isEqualTo(5);
assertThat(result.hasNext()).isTrue();
}
@Test
public void getAiModels_testSearchAndSortAppliedBeforePagination() throws Exception {
// GIVEN
loginTenantAdmin();
// Create 5 models: 3 with "Alpha" in name, 2 with "Beta" in name
var alpha1 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 1"), AiModel.class);
var beta1 = doPost("/api/ai/model", constructValidOpenAiModel("Beta Model 1"), AiModel.class);
var alpha2 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 2"), AiModel.class);
var beta2 = doPost("/api/ai/model", constructValidOpenAiModel("Beta Model 2"), AiModel.class);
var alpha3 = doPost("/api/ai/model", constructValidOpenAiModel("Alpha Model 3"), AiModel.class);
// WHEN
// Search for "Alpha", sort by name DESC, get the first page with size 2
PageData<AiModel> result = doGetTypedWithPageLink("/api/ai/model?",
new TypeReference<>() {},
new PageLink(2, 0, "Alpha", SortOrder.of("name", SortOrder.Direction.DESC)));
// THEN
// Should find only 3 "Alpha" models, sort them DESC (3, 2, 1), then return first 2
assertThat(result.getData()).containsExactly(alpha3, alpha2);
assertThat(result.getTotalPages()).isEqualTo(2); // One more "Alpha" model on the next page
assertThat(result.getTotalElements()).isEqualTo(3); // Only 3 models match "Alpha", not 5
assertThat(result.hasNext()).isTrue(); // One more "Alpha" model on the next page
}
@Test
public void getAiModels_testTextSearch() throws Exception {
// GIVEN
loginTenantAdmin();
var model1 = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 1")
.configuration(OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.modelId("o3-pro")
.build())
.build(), AiModel.class);
var model2 = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 2")
.configuration(GoogleAiGeminiChatModelConfig.builder()
.providerConfig(new GoogleAiGeminiProviderConfig("test-api-key"))
.modelId("gemini-2.5-flash")
.build())
.build(), AiModel.class);
var model3 = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 3")
.configuration(GoogleAiGeminiChatModelConfig.builder()
.providerConfig(new GoogleAiGeminiProviderConfig("test-api-key"))
.modelId("gemini-2.5-pro")
.build())
.build(), AiModel.class);
// WHEN
int pageSize = 10;
int page = 0;
SortOrder sortOrder = null;
PageData<AiModel> result1 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "google ai", sortOrder));
PageData<AiModel> result2 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "pro", sortOrder));
PageData<AiModel> result3 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "test", sortOrder));
PageData<AiModel> result4 = doGetTypedWithPageLink("/api/ai/model?", new TypeReference<>() {}, new PageLink(pageSize, page, "anthropic", sortOrder));
// THEN
// should find google models
assertThat(result1.getData()).containsExactly(model2, model3);
assertThat(result1.getTotalPages()).isEqualTo(1);
assertThat(result1.getTotalElements()).isEqualTo(2);
assertThat(result1.hasNext()).isFalse();
// should find "o3-pro" and "gemini-2.5-pro" models
assertThat(result2.getData()).containsExactly(model1, model3);
assertThat(result2.getTotalPages()).isEqualTo(1);
assertThat(result2.getTotalElements()).isEqualTo(2);
assertThat(result2.hasNext()).isFalse();
// should find all models (all contain "Test" in their names)
assertThat(result3.getData()).containsExactly(model1, model2, model3);
assertThat(result3.getTotalPages()).isEqualTo(1);
assertThat(result3.getTotalElements()).isEqualTo(3);
assertThat(result3.hasNext()).isFalse();
// should find no models (nothing matches "anthropic")
assertThat(result4.getData()).isEmpty();
assertThat(result4.getTotalPages()).isEqualTo(0);
assertThat(result4.getTotalElements()).isEqualTo(0);
assertThat(result4.hasNext()).isFalse();
}
@Test
public void getAiModels_testSortingByCreatedTime() throws Exception {
// GIVEN
loginTenantAdmin();
var model1 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 1"), AiModel.class);
var model2 = doPost("/api/ai/model", constructValidOpenAiModel("Test model 2"), AiModel.class);
// WHEN
int pageSize = 2;
int page = 0;
String textSearch = null;
PageData<AiModel> resultAsc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("createdTime", SortOrder.Direction.ASC))
);
PageData<AiModel> resultDesc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("createdTime", SortOrder.Direction.DESC))
);
// THEN
assertThat(resultAsc.getData()).containsExactly(model1, model2);
assertThat(resultAsc.getTotalPages()).isEqualTo(1);
assertThat(resultAsc.getTotalElements()).isEqualTo(2);
assertThat(resultAsc.hasNext()).isFalse();
assertThat(resultDesc.getData()).containsExactly(model2, model1);
assertThat(resultDesc.getTotalPages()).isEqualTo(1);
assertThat(resultDesc.getTotalElements()).isEqualTo(2);
assertThat(resultDesc.hasNext()).isFalse();
}
@Test
public void getAiModels_testSortingByName() throws Exception {
// GIVEN
loginTenantAdmin();
var modelA = doPost("/api/ai/model", constructValidOpenAiModel("Test model A"), AiModel.class);
var modelB = doPost("/api/ai/model", constructValidOpenAiModel("Test model B"), AiModel.class);
// WHEN
int pageSize = 2;
int page = 0;
String textSearch = null;
PageData<AiModel> resultAsc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("name", SortOrder.Direction.ASC))
);
PageData<AiModel> resultDesc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("name", SortOrder.Direction.DESC))
);
// THEN
assertThat(resultAsc.getData()).containsExactly(modelA, modelB);
assertThat(resultAsc.getTotalPages()).isEqualTo(1);
assertThat(resultAsc.getTotalElements()).isEqualTo(2);
assertThat(resultAsc.hasNext()).isFalse();
assertThat(resultDesc.getData()).containsExactly(modelB, modelA);
assertThat(resultDesc.getTotalPages()).isEqualTo(1);
assertThat(resultDesc.getTotalElements()).isEqualTo(2);
assertThat(resultDesc.hasNext()).isFalse();
}
@Test
public void getAiModels_testSortingByProvider() throws Exception {
// GIVEN
loginTenantAdmin();
var anthropicModel = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 1")
.configuration(AnthropicChatModelConfig.builder()
.providerConfig(new AnthropicProviderConfig("test-api-key"))
.modelId("claude-sonnet-4-0")
.build())
.build(), AiModel.class);
var geminiModel = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 2")
.configuration(GoogleAiGeminiChatModelConfig.builder()
.providerConfig(new GoogleAiGeminiProviderConfig("test-api-key"))
.modelId("gemini-2.5-pro")
.build())
.build(), AiModel.class);
// WHEN
int pageSize = 2;
int page = 0;
String textSearch = null;
PageData<AiModel> resultAsc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.ASC))
);
PageData<AiModel> resultDesc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.DESC))
);
// THEN
assertThat(resultAsc.getData()).containsExactly(anthropicModel, geminiModel);
assertThat(resultAsc.getTotalPages()).isEqualTo(1);
assertThat(resultAsc.getTotalElements()).isEqualTo(2);
assertThat(resultAsc.hasNext()).isFalse();
assertThat(resultDesc.getData()).containsExactly(geminiModel, anthropicModel);
assertThat(resultDesc.getTotalPages()).isEqualTo(1);
assertThat(resultDesc.getTotalElements()).isEqualTo(2);
assertThat(resultDesc.hasNext()).isFalse();
}
@Test
public void getAiModels_testSortingByModelId() throws Exception {
// GIVEN
loginTenantAdmin();
var modelA = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 1")
.configuration(AnthropicChatModelConfig.builder()
.providerConfig(new AnthropicProviderConfig("test-api-key"))
.modelId("model-a")
.build())
.build(), AiModel.class);
var modelB = doPost("/api/ai/model", AiModel.builder()
.tenantId(tenantId)
.name("Test model 2")
.configuration(GoogleAiGeminiChatModelConfig.builder()
.providerConfig(new GoogleAiGeminiProviderConfig("test-api-key"))
.modelId("model-b")
.build())
.build(), AiModel.class);
// WHEN
int pageSize = 2;
int page = 0;
String textSearch = null;
PageData<AiModel> resultAsc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("modelId", SortOrder.Direction.ASC))
);
PageData<AiModel> resultDesc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("modelId", SortOrder.Direction.DESC))
);
// THEN
assertThat(resultAsc.getData()).containsExactly(modelA, modelB);
assertThat(resultAsc.getTotalPages()).isEqualTo(1);
assertThat(resultAsc.getTotalElements()).isEqualTo(2);
assertThat(resultAsc.hasNext()).isFalse();
assertThat(resultDesc.getData()).containsExactly(modelB, modelA);
assertThat(resultDesc.getTotalPages()).isEqualTo(1);
assertThat(resultDesc.getTotalElements()).isEqualTo(2);
assertThat(resultDesc.hasNext()).isFalse();
}
@Test
public void getAiModels_testSortingByIdTieBreaker() throws Exception {
// GIVEN
loginTenantAdmin();
// Both models are from OpenAI and sorting will be done on provider
var modelA = doPost("/api/ai/model", constructValidOpenAiModel("Test model A"), AiModel.class);
var modelB = doPost("/api/ai/model", constructValidOpenAiModel("Test model B"), AiModel.class);
// WHEN
int pageSize = 2;
int page = 0;
String textSearch = null;
PageData<AiModel> resultAsc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.ASC))
);
PageData<AiModel> resultDesc = doGetTypedWithPageLink(
"/api/ai/model?", new TypeReference<>() {},
new PageLink(pageSize, page, textSearch, SortOrder.of("provider", SortOrder.Direction.DESC))
);
// THEN
// in both cases result should be the same since in case of ties (both models have OpenAI as provider, sorting by ID ascending is used)
assertThat(resultAsc.getData()).containsExactly(modelA, modelB);
assertThat(resultAsc.getTotalPages()).isEqualTo(1);
assertThat(resultAsc.getTotalElements()).isEqualTo(2);
assertThat(resultAsc.hasNext()).isFalse();
assertThat(resultDesc.getData()).containsExactly(modelA, modelB);
assertThat(resultDesc.getTotalPages()).isEqualTo(1);
assertThat(resultDesc.getTotalElements()).isEqualTo(2);
assertThat(resultDesc.hasNext()).isFalse();
}
/* --- Delete API tests --- */
@Test
public void deleteAiModelById_whenUserIsSysAdmin_shouldReturnForbidden() throws Exception {
// GIVEN
loginSysAdmin();
// WHEN
ResultActions result = doDelete("/api/ai/model/" + Uuids.timeBased());
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void deleteAiModelById_whenUserIsCustomerUser_shouldReturnForbidden() throws Exception {
// GIVEN
loginCustomerUser();
// WHEN
ResultActions result = doDelete("/api/ai/model/" + Uuids.timeBased());
// THEN
result.andExpect(status().isForbidden()).andExpect(statusReason(equalTo(msgErrorPermission)));
}
@Test
public void deleteAiModelById_whenDeletingExistingModelAsTenantAdmin_shouldSucceedAndReturnTrue() throws Exception {
// GIVEN
loginTenantAdmin();
var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class);
// WHEN
boolean deleted = doDelete("/api/ai/model/" + model.getId(), Boolean.class);
// THEN
assertThat(deleted).isTrue();
// verify model cannot be found anymore
doGet("/api/ai/model/" + model.getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(is("AI model with id [" + model.getId() + "] is not found")));
}
@Test
public void deleteAiModelById_whenDeletingNonexistentModelAsTenantAdmin_shouldSucceedAndReturnFalse() throws Exception {
// GIVEN
loginTenantAdmin();
var nonexistentModelId = new AiModelId(Uuids.timeBased());
// WHEN
boolean deleted = doDelete("/api/ai/model/" + nonexistentModelId, Boolean.class);
// THEN
assertThat(deleted).isFalse();
}
private AiModel constructValidOpenAiModel(String name) {
var modelConfig = OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
return AiModel.builder()
.tenantId(tenantId)
.name(name)
.configuration(modelConfig)
.build();
}
}

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

@ -36,10 +36,15 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.DynamicValueSourceType;
@ -230,6 +235,60 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
testCountAlarmsByQuery(alarms); testCountAlarmsByQuery(alarms);
} }
@Test
public void testTenantCountAlarmsWithEntityFilter() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
Device savedDevice = doPost("/api/device", device, Device.class);
devices.add(savedDevice);
Thread.sleep(1);
Alarm alarm = new Alarm();
alarm.setOriginator(savedDevice.getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setName("Asset" + i);
asset.setType("default");
asset.setLabel("testLabel" + (int) (Math.random() * 1000));
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
assets.add(savedAsset);
Thread.sleep(1);
Alarm alarm = new Alarm();
alarm.setOriginator(savedAsset.getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter);
Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class);
Assert.assertEquals(assets.size(), assetAlamCount.longValue());
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmCountQuery filteredAssetAlarmQuery = new AlarmCountQuery(assetTypeFilter, keyFilters);
Long filteredAssetAlamCount = doPostWithResponse("/api/alarmsQuery/count", filteredAssetAlarmQuery, Long.class);
Assert.assertEquals(1, filteredAssetAlamCount.longValue());
}
@Test @Test
public void testCustomerCountAlarmsByQuery() throws Exception { public void testCustomerCountAlarmsByQuery() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
@ -259,6 +318,213 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
testCountAlarmsByQuery(alarms); testCountAlarmsByQuery(alarms);
} }
@Test
public void testCustomerCountAlarmsWithEntityFilter() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setCustomerId(customerId);
asset.setName("Asset" + i);
asset.setType("default");
asset.setLabel("testLabel" + (int) (Math.random() * 1000));
assets.add(doPost("/api/asset", asset, Asset.class));
Thread.sleep(1);
}
loginCustomerUser();
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
for (int i = 0; i < assets.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(assets.get(i).getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmCountQuery assetAlarmQuery = new AlarmCountQuery(assetTypeFilter);
Long assetAlamCount = doPostWithResponse("/api/alarmsQuery/count", assetAlarmQuery, Long.class);
Assert.assertEquals(10, assetAlamCount.longValue());
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmCountQuery filteredAssetAlarmQuery = new AlarmCountQuery(assetTypeFilter, keyFilters);
Long filteredAssetAlamCount = doPostWithResponse("/api/alarmsQuery/count", filteredAssetAlarmQuery, Long.class);
Assert.assertEquals(1, filteredAssetAlamCount.longValue());
}
@Test
public void testFindTenantAlarmsWithEntityFilter() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setCustomerId(customerId);
asset.setName("Asset" + i);
asset.setType("default");
asset.setLabel("testLabel" + (int) (Math.random() * 1000));
assets.add(doPost("/api/asset", asset, Asset.class));
Thread.sleep(1);
}
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setOriginator(devices.get(i).getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
List<String> assetAlarmTypes = new ArrayList<>();
for (int i = 0; i < assets.size(); i++) {
Alarm alarm = new Alarm();
alarm.setOriginator(assets.get(i).getId());
String type = "asset alarm" + i;
alarm.setType(type);
assetAlarmTypes.add(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "assignee")));
List<EntityKey> alarmFields = new ArrayList<>();
alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type"));
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields);
PageData<AlarmData> alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(10, alarmPageData.getTotalElements());
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields);
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
@Test
public void testFindCustomerAlarmsWithEntityFilter() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
List<Asset> assets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Asset asset = new Asset();
asset.setCustomerId(customerId);
asset.setName("Asset" + i);
asset.setType("default");
asset.setLabel("testLabel" + (int) (Math.random() * 1000));
assets.add(doPost("/api/asset", asset, Asset.class));
Thread.sleep(1);
}
loginCustomerUser();
for (int i = 0; i < devices.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(devices.get(i).getId());
alarm.setType("alarm" + i);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
List<String> assetAlarmTypes = new ArrayList<>();
for (int i = 0; i < assets.size(); i++) {
Alarm alarm = new Alarm();
alarm.setCustomerId(customerId);
alarm.setOriginator(assets.get(i).getId());
String type = "asset alarm" + i;
alarm.setType(type);
assetAlarmTypes.add(type);
alarm.setSeverity(AlarmSeverity.WARNING);
doPost("/api/alarm", alarm, Alarm.class);
Thread.sleep(1);
}
AlarmDataPageLink pageLink = new AlarmDataPageLink();
pageLink.setPage(0);
pageLink.setPageSize(100);
pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "assignee")));
EntityTypeFilter assetTypeFilter = new EntityTypeFilter();
assetTypeFilter.setEntityType(EntityType.ASSET);
AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList());
PageData<AlarmData> alarmPageData = doPostWithTypedResponse("/api/alarmsQuery/find", assetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(10, alarmPageData.getTotalElements());
List<String> retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList();
assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes);
KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1");
List<KeyFilter> keyFilters = Collections.singletonList(nameFilter);
AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList());
PageData<AlarmData> filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() {
});
Assert.assertEquals(1, filteredAssetAlamData.getTotalElements());
}
private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception { private void testCountAlarmsByQuery(List<Alarm> alarms) throws Exception {
AlarmCountQuery countQuery = new AlarmCountQuery(); AlarmCountQuery countQuery = new AlarmCountQuery();
@ -442,7 +708,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
} }
RelationsQueryFilter filter = new RelationsQueryFilter(); RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(mainDevice.getId()); filter.setRootEntity(AliasEntityId.fromEntityId(mainDevice.getId()));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
filter.setNegate(true); filter.setNegate(true);
filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false))); filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false)));
@ -913,4 +1179,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
return numericFilter; return numericFilter;
} }
private KeyFilter buildStringKeyFilter(EntityKeyType entityKeyType, String name, StringFilterPredicate.StringOperation operation, String value) {
KeyFilter nameFilter = new KeyFilter();
nameFilter.setKey(new EntityKey(entityKeyType, name));
StringFilterPredicate predicate = new StringFilterPredicate();
predicate.setOperation(operation);
predicate.setValue(FilterPredicateValue.fromString(value));
nameFilter.setPredicate(predicate);
return nameFilter;
}
} }

16
application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java

@ -19,10 +19,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -32,6 +34,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES; import static org.thingsboard.server.common.data.query.EntityKeyType.TIME_SERIES;
@ -115,7 +118,7 @@ public class TelemetryControllerTest extends AbstractControllerTest {
Device device = createDevice(); Device device = createDevice();
SingleEntityFilter filter = new SingleEntityFilter(); SingleEntityFilter filter = new SingleEntityFilter();
filter.setSingleEntity(device.getId()); filter.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
@ -159,7 +162,7 @@ public class TelemetryControllerTest extends AbstractControllerTest {
Device device = createDevice(); Device device = createDevice();
SingleEntityFilter filter = new SingleEntityFilter(); SingleEntityFilter filter = new SingleEntityFilter();
filter.setSingleEntity(device.getId()); filter.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter); getWsClient().subscribeLatestUpdate(List.of(new EntityKey(TIME_SERIES, "data")), filter);
@ -207,6 +210,15 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
} }
@Test
public void testBadRequestReturnedWhenMethodArgumentTypeMismatch() throws Exception {
loginTenantAdmin();
String content = "{\"key\": \"value\"}";
doPost("/api/plugins/telemetry/DEVICE/20b559f5-849f-4361-b4f6-b6d0b76687e9/INVALID_SCOPE", content, (String) null)
.andExpect(status().isBadRequest())
.andExpect(result -> assertThat(result.getResolvedException()).isInstanceOf(MethodArgumentTypeMismatchException.class));
}
@Test @Test
public void testEmptyKeyIsProhibited() throws Exception { public void testEmptyKeyIsProhibited() throws Exception {
loginTenantAdmin(); loginTenantAdmin();

7
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
@ -332,7 +333,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
loginTenantAdmin(); loginTenantAdmin();
SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(tenantId); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId));
AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter); AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter);
AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery); AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
@ -356,7 +357,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertEquals(1, update.getCount()); Assert.assertEquals(1, update.getCount());
// set wrong entity id in filter, check count = 0 // set wrong entity id in filter, check count = 0
singleEntityFilter.setSingleEntity(tenantAdminUserId); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantAdminUserId));
AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery); AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery);
getWsClient().send(cmd3); getWsClient().send(cmd3);
@ -865,7 +866,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
public void testAttributesSubscription_sysAdmin() throws Exception { public void testAttributesSubscription_sysAdmin() throws Exception {
loginSysAdmin(); loginSysAdmin();
SingleEntityFilter entityFilter = new SingleEntityFilter(); SingleEntityFilter entityFilter = new SingleEntityFilter();
entityFilter.setSingleEntity(tenantId); entityFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantId));
assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> { assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> {
JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr")); JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr"));

33
application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java

@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TimeseriesSaveResult; import org.thingsboard.server.common.data.kv.TimeseriesSaveResult;
import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.ApiUsageStateFilter; import org.thingsboard.server.common.data.query.ApiUsageStateFilter;
import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; import org.thingsboard.server.common.data.query.AssetSearchQueryFilter;
import org.thingsboard.server.common.data.query.AssetTypeFilter; import org.thingsboard.server.common.data.query.AssetTypeFilter;
@ -217,7 +218,7 @@ public class EntityServiceTest extends AbstractControllerTest {
createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
RelationsQueryFilter filter = new RelationsQueryFilter(); RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
EntityCountQuery countQuery = new EntityCountQuery(filter); EntityCountQuery countQuery = new EntityCountQuery(filter);
@ -226,13 +227,13 @@ public class EntityServiceTest extends AbstractControllerTest {
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
countByQueryAndCheck(countQuery, 25); countByQueryAndCheck(countQuery, 25);
filter.setRootEntity(devices.get(0).getId()); filter.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId()));
filter.setDirection(EntitySearchDirection.TO); filter.setDirection(EntitySearchDirection.TO);
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT))));
countByQueryAndCheck(countQuery, 1); countByQueryAndCheck(countQuery, 1);
DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter(); DeviceSearchQueryFilter filter2 = new DeviceSearchQueryFilter();
filter2.setRootEntity(tenantId); filter2.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter2.setDirection(EntitySearchDirection.FROM); filter2.setDirection(EntitySearchDirection.FROM);
filter2.setRelationType("Contains"); filter2.setRelationType("Contains");
@ -242,12 +243,12 @@ public class EntityServiceTest extends AbstractControllerTest {
filter2.setDeviceTypes(Arrays.asList("default0", "default1")); filter2.setDeviceTypes(Arrays.asList("default0", "default1"));
countByQueryAndCheck(countQuery, 10); countByQueryAndCheck(countQuery, 10);
filter2.setRootEntity(devices.get(0).getId()); filter2.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId()));
filter2.setDirection(EntitySearchDirection.TO); filter2.setDirection(EntitySearchDirection.TO);
countByQueryAndCheck(countQuery, 0); countByQueryAndCheck(countQuery, 0);
AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter(); AssetSearchQueryFilter filter3 = new AssetSearchQueryFilter();
filter3.setRootEntity(tenantId); filter3.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter3.setDirection(EntitySearchDirection.FROM); filter3.setDirection(EntitySearchDirection.FROM);
filter3.setRelationType("Manages"); filter3.setRelationType("Manages");
@ -257,7 +258,7 @@ public class EntityServiceTest extends AbstractControllerTest {
filter3.setAssetTypes(Arrays.asList("type0", "type1")); filter3.setAssetTypes(Arrays.asList("type0", "type1"));
countByQueryAndCheck(countQuery, 2); countByQueryAndCheck(countQuery, 2);
filter3.setRootEntity(devices.get(0).getId()); filter3.setRootEntity(AliasEntityId.fromEntityId(devices.get(0).getId()));
filter3.setDirection(EntitySearchDirection.TO); filter3.setDirection(EntitySearchDirection.TO);
countByQueryAndCheck(countQuery, 0); countByQueryAndCheck(countQuery, 0);
} }
@ -268,7 +269,7 @@ public class EntityServiceTest extends AbstractControllerTest {
createTestUserRelations(tenantId, users); createTestUserRelations(tenantId, users);
RelationsQueryFilter filter = new RelationsQueryFilter(); RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null); EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null);
@ -352,7 +353,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter(); EdgeSearchQueryFilter filter = new EdgeSearchQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
filter.setRelationType("Manages"); filter.setRelationType("Manages");
@ -404,7 +405,7 @@ public class EntityServiceTest extends AbstractControllerTest {
Futures.allAsList(attributeFutures).get(); Futures.allAsList(attributeFutures).get();
RelationsQueryFilter filter = new RelationsQueryFilter(); RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE))));
filter.setMaxLevel(maxLevel); filter.setMaxLevel(maxLevel);
@ -554,7 +555,7 @@ public class EntityServiceTest extends AbstractControllerTest {
Futures.allAsList(attributeFutures).get(); Futures.allAsList(attributeFutures).get();
DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter(); DeviceSearchQueryFilter filter = new DeviceSearchQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
filter.setRelationType("Contains"); filter.setRelationType("Contains");
filter.setMaxLevel(2); filter.setMaxLevel(2);
@ -608,7 +609,7 @@ public class EntityServiceTest extends AbstractControllerTest {
Futures.allAsList(attributeFutures).get(); Futures.allAsList(attributeFutures).get();
AssetSearchQueryFilter filter = new AssetSearchQueryFilter(); AssetSearchQueryFilter filter = new AssetSearchQueryFilter();
filter.setRootEntity(tenantId); filter.setRootEntity(AliasEntityId.fromEntityId(tenantId));
filter.setDirection(EntitySearchDirection.FROM); filter.setDirection(EntitySearchDirection.FROM);
filter.setRelationType("Manages"); filter.setRelationType("Manages");
@ -1101,7 +1102,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(devices.get(0).getId()); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(devices.get(0).getId()));
List<EntityKey> entityFields = List.of( List<EntityKey> entityFields = List.of(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name") new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
@ -1120,7 +1121,7 @@ public class EntityServiceTest extends AbstractControllerTest {
@Test @Test
public void testFindCustomerBySingleEntityFilter() { public void testFindCustomerBySingleEntityFilter() {
SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(customerId); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(customerId));
List<EntityKey> entityFields = List.of( List<EntityKey> entityFields = List.of(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name") new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
); );
@ -1192,7 +1193,7 @@ public class EntityServiceTest extends AbstractControllerTest {
List<KeyFilter> keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device "); List<KeyFilter> keyFiltersEqualString = createStringKeyFilters("name", EntityKeyType.ENTITY_FIELD, StringOperation.STARTS_WITH, "Test device ");
for (Asset asset : assets) { for (Asset asset : assets) {
filter.setRootEntity(asset.getId()); filter.setRootEntity(AliasEntityId.fromEntityId(asset.getId()));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString); EntityDataQuery query = new EntityDataQuery(filter, pageLink, Collections.emptyList(), Collections.emptyList(), keyFiltersEqualString);
findByQueryAndCheck(customer.getId(), query, relationsCnt); findByQueryAndCheck(customer.getId(), query, relationsCnt);
@ -1384,7 +1385,7 @@ public class EntityServiceTest extends AbstractControllerTest {
} }
SingleEntityFilter singleEntityFilter = new SingleEntityFilter(); SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(customerDevices.get(0).getId()); singleEntityFilter.setSingleEntity(AliasEntityId.fromEntityId(customerDevices.get(0).getId()));
List<EntityKey> entityFields = List.of( List<EntityKey> entityFields = List.of(
new EntityKey(EntityKeyType.ENTITY_FIELD, "name") new EntityKey(EntityKeyType.ENTITY_FIELD, "name")
); );
@ -1403,7 +1404,7 @@ public class EntityServiceTest extends AbstractControllerTest {
// try to find tenant device by customer user // try to find tenant device by customer user
SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter(); SingleEntityFilter tenantDeviceFilter = new SingleEntityFilter();
tenantDeviceFilter.setSingleEntity(tenantDevices.get(0).getId()); tenantDeviceFilter.setSingleEntity(AliasEntityId.fromEntityId(tenantDevices.get(0).getId()));
EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null); EntityDataQuery customerQuery2 = new EntityDataQuery(tenantDeviceFilter, pageLink, entityFields, null, null);
findByQueryAndCheck(customerId, customerQuery2, 0); findByQueryAndCheck(customerId, customerQuery2, 0);
} }

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

@ -0,0 +1,268 @@
/**
* 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.entitiy.ai;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.ai.AiModel;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.ai.AiModelService;
import org.thingsboard.server.service.entitiy.TbLogEntityActionService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.never;
@ExtendWith(MockitoExtension.class)
class DefaultTbAiModelServiceTest {
@Mock
EntitiesVersionControlService vcServiceMock;
@Mock
AiModelService aiModelServiceMock;
@Mock
TbLogEntityActionService logEntityActionServiceMock;
@Spy
@InjectMocks
DefaultTbAiModelService service;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
User user;
@BeforeEach
void setUp() {
user = new User();
user.setTenantId(tenantId);
service = new DefaultTbAiModelService(aiModelServiceMock);
ReflectionTestUtils.setField(service, "vcService", vcServiceMock);
ReflectionTestUtils.setField(service, "logEntityActionService", logEntityActionServiceMock);
}
@Test
void save_whenCreatingNewModel_shouldAutoCommitAndLogAddedActionAndUseTenantIdFromUser() {
// GIVEN
var modelToSave = AiModel.builder()
.name("Model to save")
.configuration(constructValidOpenAiModelConfig())
.build();
var savedModel = new AiModel(modelToSave);
savedModel.setId(new AiModelId(UUID.randomUUID()));
savedModel.setTenantId(user.getTenantId());
savedModel.setVersion(1L);
savedModel.setCreatedTime(System.currentTimeMillis());
given(aiModelServiceMock.save(modelToSave)).willReturn(savedModel);
// WHEN
AiModel result = service.save(modelToSave, user);
// THEN
assertThat(result).isEqualTo(savedModel);
then(aiModelServiceMock).should().save(modelToSave);
then(vcServiceMock).should().autoCommit(user, savedModel.getId());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, savedModel.getId(), savedModel, ActionType.ADDED, user);
}
@Test
void save_whenUpdatingExistingModel_shouldAutoCommitAndLogUpdatedAction() {
// GIVEN
var modelToUpdate = AiModel.builder()
.tenantId(tenantId)
.version(1L)
.name("Model to update")
.configuration(constructValidOpenAiModelConfig())
.build();
modelToUpdate.setId(new AiModelId(UUID.randomUUID()));
modelToUpdate.setCreatedTime(System.currentTimeMillis());
var updatedModel = new AiModel(modelToUpdate);
updatedModel.setVersion(2L);
updatedModel.setName("Updated model");
given(aiModelServiceMock.save(modelToUpdate)).willReturn(updatedModel);
// WHEN
AiModel result = service.save(modelToUpdate, user);
// THEN
assertThat(result).isEqualTo(updatedModel);
then(aiModelServiceMock).should().save(modelToUpdate);
then(vcServiceMock).should().autoCommit(user, updatedModel.getId());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, updatedModel.getId(), updatedModel, ActionType.UPDATED, user);
}
@Test
void save_whenCreatingNewModelThrowsException_shouldUseEmptyIdAndLogError() {
// GIVEN
var modelToSave = AiModel.builder()
.tenantId(tenantId)
.name("Model to save")
.configuration(constructValidOpenAiModelConfig())
.build();
var exception = new RuntimeException("Failed to save");
given(aiModelServiceMock.save(modelToSave)).willThrow(exception);
// WHEN-THEN
assertThatThrownBy(() -> service.save(modelToSave, user))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to save");
then(aiModelServiceMock).should().save(modelToSave);
then(vcServiceMock).should(never()).autoCommit(any(), any());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, new AiModelId(EntityId.NULL_UUID), modelToSave, ActionType.ADDED, user, exception);
}
@Test
void save_whenUpdatingExistingModelThrowsException_shouldUseExistingModelIdAndLogError() {
// GIVEN
var modelToUpdate = AiModel.builder()
.tenantId(tenantId)
.version(1L)
.name("Model to update")
.configuration(constructValidOpenAiModelConfig())
.build();
modelToUpdate.setId(new AiModelId(UUID.randomUUID()));
modelToUpdate.setCreatedTime(System.currentTimeMillis());
var exception = new RuntimeException("Failed to save");
given(aiModelServiceMock.save(modelToUpdate)).willThrow(exception);
// WHEN-THEN
assertThatThrownBy(() -> service.save(modelToUpdate, user))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to save");
then(aiModelServiceMock).should().save(modelToUpdate);
then(vcServiceMock).should(never()).autoCommit(any(), any());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToUpdate.getId(), modelToUpdate, ActionType.UPDATED, user, exception);
}
@Test
void delete_whenDeleteSuccessful_shouldLogDeletedAction() {
// GIVEN
var modelToDelete = AiModel.builder()
.tenantId(tenantId)
.version(1L)
.name("Model to delete")
.configuration(constructValidOpenAiModelConfig())
.build();
modelToDelete.setId(new AiModelId(UUID.randomUUID()));
modelToDelete.setCreatedTime(System.currentTimeMillis());
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(true);
// WHEN
boolean result = service.delete(modelToDelete, user);
// THEN
assertThat(result).isTrue();
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString());
}
@Test
void delete_whenDeleteReturnsFalse_shouldNotLogAction() {
// GIVEN
var modelToDelete = AiModel.builder()
.tenantId(tenantId)
.version(1L)
.name("Model to delete")
.configuration(constructValidOpenAiModelConfig())
.build();
modelToDelete.setId(new AiModelId(UUID.randomUUID()));
modelToDelete.setCreatedTime(System.currentTimeMillis());
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willReturn(false);
// WHEN
boolean result = service.delete(modelToDelete, user);
// THEN
assertThat(result).isFalse();
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId());
then(logEntityActionServiceMock).should(never()).logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, modelToDelete.getId().toString());
}
@Test
void delete_whenDeleteThrowsException_shouldLogError() {
// GIVEN
var modelToDelete = AiModel.builder()
.tenantId(tenantId)
.version(1L)
.name("Model to delete")
.configuration(constructValidOpenAiModelConfig())
.build();
modelToDelete.setId(new AiModelId(UUID.randomUUID()));
modelToDelete.setCreatedTime(System.currentTimeMillis());
var exception = new RuntimeException("Failed to delete");
given(aiModelServiceMock.deleteByTenantIdAndId(tenantId, modelToDelete.getId())).willThrow(exception);
// WHEN-THEN
assertThatThrownBy(() -> service.delete(modelToDelete, user))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("Failed to delete");
then(aiModelServiceMock).should().deleteByTenantIdAndId(tenantId, modelToDelete.getId());
then(logEntityActionServiceMock).should().logEntityAction(tenantId, modelToDelete.getId(), modelToDelete, ActionType.DELETED, user, exception, modelToDelete.getId().toString());
}
private static AiModelConfig constructValidOpenAiModelConfig() {
return OpenAiChatModelConfig.builder()
.providerConfig(new OpenAiProviderConfig("test-api-key"))
.modelId("gpt-4o")
.temperature(0.5)
.topP(0.3)
.frequencyPenalty(0.1)
.presencePenalty(0.2)
.maxOutputTokens(1000)
.timeoutSeconds(60)
.maxRetries(2)
.build();
}
}

101
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -31,6 +31,8 @@ import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -77,6 +79,8 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.entity.EntityServiceRegistry;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
@ -145,6 +149,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
private ApiUsageStateDao apiUsageStateDao; private ApiUsageStateDao apiUsageStateDao;
@Autowired @Autowired
private EntityServiceRegistry entityServiceRegistry; private EntityServiceRegistry entityServiceRegistry;
@Autowired
private CustomerService customerService;
@Autowired
private DashboardService dashboardService;
@SpyBean @SpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor; private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
@ -238,11 +246,67 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(device.getId()); verifyNoAlarms(device.getId());
}); });
} }
@Test
public void whenAssetIsDeleted_thenDeleteAllAlarms() throws Exception {
Asset asset = createAsset();
for (int i = 1; i <= 1000; i++) {
createAlarm(asset.getId());
}
doDelete("/api/asset/" + asset.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(asset.getId());
});
}
@Test
public void whenDashboardIsDeleted_thenDeleteAllAlarms() throws Exception {
Dashboard dashboard = createDashboard();
for (int i = 1; i <= 1000; i++) {
createAlarm(dashboard.getId());
}
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(dashboard.getId());
});
}
@Test
public void whenCustomerIsDeleted_thenDeleteAllAlarms() throws Exception {
Customer customer = createCustomer();
for (int i = 1; i <= 1000; i++) {
createAlarm(customer.getId());
}
doDelete("/api/customer/" + customer.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(customer.getId());
});
}
@Test
public void whenUserIsDeleted_thenDeleteAllAlarms() throws Exception {
UserId userId = customerUserId;
for (int i = 1; i <= 1000; i++) {
createAlarm(userId);
}
doDelete("/api/user/" + userId).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(userId);
});
}
@Test @Test
public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception { public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception {
loginDifferentTenant(); loginDifferentTenant();
@ -335,7 +399,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int attempts = 2; int attempts = 2;
await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 0; i <= attempts; i++) { for (int i = 0; i <= attempts; i++) {
int attempt = i; int attempt = i;
verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY,
@ -345,7 +409,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); assertThat(getTimeseriesHistory(device.getId())).isNotEmpty();
doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any());
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(getTimeseriesHistory(device.getId())).isEmpty(); assertThat(getTimeseriesHistory(device.getId())).isEmpty();
}); });
} }
@ -379,7 +443,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int attempts = 2; int attempts = 2;
await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 0; i <= attempts; i++) { for (int i = 0; i <= attempts; i++) {
int attempt = i; int attempt = i;
verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY,
@ -393,7 +457,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any());
someExecutor.shutdown(); someExecutor.shutdown();
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(getTimeseriesHistory(device.getId())).isEmpty(); assertThat(getTimeseriesHistory(device.getId())).isEmpty();
}); });
} }
@ -409,7 +473,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int maxAttempts = 5; int maxAttempts = 5;
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 1; i <= maxAttempts; i++) { for (int i = 1; i <= maxAttempts; i++) {
verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i); verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i);
} }
@ -479,7 +543,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
eventService.saveAsync(event); eventService.saveAsync(event);
await().atMost(10, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> !getEvents(entityId).isEmpty()); .until(() -> !getEvents(entityId).isEmpty());
} }
private void createRelation(DeviceId to, AssetId from) { private void createRelation(DeviceId to, AssetId from) {
@ -502,14 +565,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty();
} }
private void createAlarm(DeviceId deviceId) { private void createAlarm(EntityId entityId) {
Alarm alarm = doPost("/api/alarm", Alarm.builder() doPost("/api/alarm", Alarm.builder()
.tenantId(tenantId) .tenantId(tenantId)
.originator(deviceId) .originator(entityId)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) .type("test alarm for " + entityId + " " + RandomStringUtils.randomAlphabetic(10))
.build(), Alarm.class); .build(), Alarm.class);
assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, 0, null, 10)).isNotEmpty();
} }
private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception {
@ -534,6 +597,20 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
return doPost("/api/asset", asset, Asset.class); return doPost("/api/asset", asset, Asset.class);
} }
private Customer createCustomer() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle(StringUtils.randomAlphabetic(10));
return customerService.saveCustomer(customer);
}
private Dashboard createDashboard() {
Dashboard dashboard = new Dashboard();
dashboard.setTenantId(tenantId);
dashboard.setTitle(StringUtils.randomAlphabetic(10));
return dashboardService.saveDashboard(dashboard);
}
private RuleChainMetaData createRuleChain() { private RuleChainMetaData createRuleChain() {
RuleChain ruleChain = new RuleChain(); RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId); ruleChain.setTenantId(tenantId);

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

@ -26,8 +26,10 @@ import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -750,6 +752,284 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
assertEquals(expected, actual); assertEquals(expected, actual);
} }
// Sets
@Test
public void setsCreateNewSetFromMap_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["B", "A", "C", "A"]}
""";
decoderStr = """
var originalMap = {};
var set1 = originalMap.entrySet(); // create new Set from map, Empty
var set2 = set1.clone(); // clone new Set, Empty
var result1 = set1.addAll(msg.list); // addAll list, no sort, size = 3 ("A" - duplicate)
return {set1: set1,
set2: set2,
result1: result1
}
""";
Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A"));
Set expectedSet2 = new LinkedHashSet();
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("set1", expectedSet1);
expected.put("set2", expectedSet2);
expected.put("result1", true);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected.toString(), actual.toString());
}
@Test
public void setsCreateNewSetFromCreateSetTbMethod_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["B", "A", "C", "A"]}
""";
decoderStr = """
var set1 = toSet(msg.list); // create new Set from toSet() with list, no sort, size = 3 ("A" - duplicate)
var set2 = newSet(); // create new Set from newSet(), Empty
return {set1: set1,
set2: set2
}
""";
Set expectedSet1 = new LinkedHashSet(List.of("B", "A", "C", "A"));
Set expectedSet2 = new LinkedHashSet();
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("set1", expectedSet1);
expected.put("set2", expectedSet2);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected.toString(), actual.toString());
}
@Test
public void setsForeachForLoop_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["A", "B", "C"]}
""";
decoderStr = """
var set2 = toSet(msg.list); // create new from list, size = 3
var set2_0 = set2.toArray()[0]; // return "A", value with index = 0 from Set
var set2Size = set2.size(); // return size = 3
var smthForeach = "";
foreach (item : set2) { // foreach for Set
smthForeach += item; // return "ABC"
}
var smthForLoop= "";
var set2Array = set2.toArray(); // for loop for Set (Set to array))
for (var i =0; i < set2.size; i++) {
smthForLoop += set2Array[i]; // return "ABC"
}
return {
set2: set2,
set2_0: set2_0,
set2Size: set2Size,
smthForeach: smthForeach,
smthForLoop: smthForLoop
}
""";
Set expectedSet2 = new LinkedHashSet(List.of("A", "B", "C"));
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("set2", expectedSet2);
expected.put("set2_0", expectedSet2.toArray()[0]);
expected.put("set2Size", expectedSet2.size());
AtomicReference<String> smth = new AtomicReference<>("");
expectedSet2.forEach(s -> smth.updateAndGet(v -> v + s));
expected.put("smthForeach", smth.get());
expected.put("smthForLoop", smth.get());
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected.toString(), actual.toString());
}
/**
* add
* delete/remove
* setCreate, setCreatList
*/
@Test
public void setsAddRemove_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["B", "C", "A", "B", "C", "hello", 34567]}
""";
decoderStr = """
// add
var setAdd = toSet(["thigsboard", 4, 67]); // create new, size = 3
var setAdd1_value = setAdd.clone(); // clone setAdd, size = 3
var setAdd2_result = setAdd.add(35); // add value = 35, result = true
var setAdd2_value = setAdd.clone(); // clone setAdd (fixing the result add = 35), size = 4
var setAddList1 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), size = 5
var setAdd3_result = setAdd.addAll(setAddList1); // add all without duplicate values, result = true
var setAdd3_value = setAdd.clone(); // clone setAdd (with addAll), size = 9
var setAdd4_result = setAdd.add(35); // add duplicate value = 35, result = false
var setAdd4_value = setAdd.clone(); // clone setAdd (after add duplicate value = 35), size = 9
var setAddList2 = toSet(msg.list); // create new from list without duplicate value ("B" and "C" - only one), start: size = 5, finish: size = 7
var setAdd5_result1 = setAddList2.add(72); // add is not duplicate value = 72, result = true
var setAdd5_result2 = setAddList2.add(72); // add duplicate value = 72, result = false
var setAdd5_result3 = setAddList2.add("hello25"); // add is not duplicate value = "hello25", result = true
var setAdd5_value = setAddList2.clone(); // clone setAddList2, size = 7
var setAdd6_result = setAdd.addAll(setAddList2); // add all with duplicate values, result = true
var setAdd6_value = setAdd.clone(); // clone setAdd (after addAll setAddList2), before size = 9, after size = 11, added only is not duplicate values {"hello25", 72}
// remove
var setAdd7_value = setAdd6_value.clone(); // clone setAdd6_value, before size = 11, after remove value = 4 size = 10
var setAdd7_result = setAdd7_value.remove(4); // remove value = 4, result = true
var setAdd8_value = setAdd7_value.clone(); // clone setAdd7_value, before size = 10, after clear size = 0
setAdd8_value.clear(); // setAdd8_value clear, result size = 0
return {
"setAdd1_value": setAdd1_value,
"setAdd2_result": setAdd2_result,
"setAdd2_value": setAdd2_value,
"setAddList1": setAddList1,
"setAdd3_result": setAdd3_result,
"setAdd3_value": setAdd3_value,
"setAdd4_result": setAdd4_result,
"setAdd4_value": setAdd4_value,
"setAdd5_result1": setAdd5_result1,
"setAdd5_result2": setAdd5_result2,
"setAdd5_result3": setAdd5_result3,
"setAddList2": setAddList2,
"setAdd5_value": setAdd5_value,
"setAdd6_result": setAdd6_result,
"setAdd6_value": setAdd6_value,
"setAdd7_result": setAdd7_result,
"setAdd7_value": setAdd7_value,
"setAdd8_value": setAdd8_value
};
""";
ArrayList<Object> list = new ArrayList<>(List.of("B", "C", "A", "B", "C", "hello", 34567));
ArrayList<Object> listAdd = new ArrayList<>(List.of("thigsboard", 4, 67));
Set<Object> setAdd = new LinkedHashSet<>(listAdd);
Set setAdd1_value = new LinkedHashSet<>(setAdd);
boolean setAdd2_result = setAdd.add(35);
Set<Object> setAdd2_value = new LinkedHashSet<>(setAdd);
Set<Object> setAddList1 = new LinkedHashSet<>(list);
boolean setAdd3_result = setAdd.addAll(setAddList1);
Set<Object> setAdd3_value = new LinkedHashSet<>(setAdd);
boolean setAdd4_result = setAdd.add(35);
Set<Object> setAdd4_value = new LinkedHashSet<>(setAdd);
Set<Object> setAddList2 = new LinkedHashSet<>(list);
boolean setAdd5_result1 = setAddList2.add(72);
boolean setAdd5_result2 = setAddList2.add(72);
boolean setAdd5_result3 = setAddList2.add("hello25");
Set<Object> setAdd5_value = new LinkedHashSet<>(setAddList2);
boolean setAdd6_result = setAdd.addAll(setAddList2);
Set<Object> setAdd6_value = new LinkedHashSet<>(setAdd);
// remove
Set<Object> setAdd7_value = new LinkedHashSet<>(setAdd6_value);
boolean setAdd7_result = setAdd7_value.remove(4);
Set<Object> setAdd8_value = new LinkedHashSet<>(setAdd7_value);
setAdd8_value.clear();
LinkedHashMap<String, Object> expected = new LinkedHashMap<>();
expected.put("setAdd1_value", setAdd1_value);
expected.put("setAdd2_result", setAdd2_result);
expected.put("setAdd2_value", setAdd2_value);
expected.put("setAddList1", setAddList1);
expected.put("setAdd3_result", setAdd3_result);
expected.put("setAdd3_value", setAdd3_value);
expected.put("setAdd4_result", setAdd4_result);
expected.put("setAdd4_value", setAdd4_value);
expected.put("setAdd5_result1", setAdd5_result1);
expected.put("setAdd5_result2", setAdd5_result2);
expected.put("setAdd5_result3", setAdd5_result3);
expected.put("setAddList2", setAddList2);
expected.put("setAdd5_value", setAdd5_value);
expected.put("setAdd6_result", setAdd6_result);
expected.put("setAdd6_value", setAdd6_value);
expected.put("setAdd7_result", setAdd7_result);
expected.put("setAdd7_value", setAdd7_value);
expected.put("setAdd8_value", setAdd8_value);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expected.toString(), actual.toString());
}
@Test
public void setsSort_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]}
""";
decoderStr = """
var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated)
var set2 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated)
var set1_asc = set1.clone(); // clone set1, size = 6
var set1_desc = set1.clone(); // clone set1, size = 6
set1.sort(); // sort set1 -> asc
set1_asc.sort(true); // sort set1_asc -> asc
set1_desc.sort(false); // sort set1_desc -> desc
var set3 = set2.toSorted(); // toSorted set3 -> asc
var set3_asc = set2.toSorted(true); // toSorted set3 -> asc
var set3_desc = set2.toSorted(false); // toSorted set3 -> desc
return {
"set1": set1,
"set1_asc": set1_asc,
"set1_desc": set1_desc,
"set2": set2,
"set3": set3,
"set3_asc": set3_asc,
"set3_desc": set3_desc,
}
""";
ArrayList<Object> list = new ArrayList<>(List.of("C", "B", "A", 34567, "hello", 34));
Set<Object> expected = new LinkedHashSet<>(list);
ArrayList<Object> listSortAsc = new ArrayList<>(List.of(34, 34567, "A", "B", "C", "hello"));
Set<Object> expectedAsc = new LinkedHashSet<>(listSortAsc);
ArrayList<Object> listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34));
Set<Object> expectedDesc = new LinkedHashSet<>(listSortDesc);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>)actual).get("set1_desc").toString());
assertEquals(expected.toString(), ((LinkedHashMap<?, ?>)actual).get("set2").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3").toString());
assertEquals(expectedAsc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3_asc").toString());
assertEquals(expectedDesc.toString(), ((LinkedHashMap<?, ?>)actual).get("set3_desc").toString());
}
@Test
public void setsContains_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]}
""";
decoderStr = """
var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated)
var result1 = set1.contains("A"); // return true
var result2 = set1.contains("H"); // return false
return {
"set1": set1,
"result1": result1,
"result2": result2
}
""";
List<Object> listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34));
Set<Object> expectedSet = new LinkedHashSet<>(listOrigin);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(true, ((LinkedHashMap<?, ?>)actual).get("result1"));
assertEquals(false, ((LinkedHashMap<?, ?>)actual).get("result2"));
}
@Test
public void setsToList_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]}
""";
decoderStr = """
var set1 = toSet(msg.list); // create new from method toSet(List list) no sort, size = 6 ("A" and "C" is duplicated)
var tolist = set1.toList(); // create new List from Set, size = 6
return {
"list": msg.list,
"set1": set1,
"tolist": tolist
}
""";
List<Object> listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34));
Set<Object> expectedSet = new LinkedHashSet<>(listOrigin);
List<Object> expectedToList = new ArrayList<>(expectedSet);
Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertEquals(listOrigin.toString(), ((LinkedHashMap<?, ?>)actual).get("list").toString());
assertEquals(expectedSet.toString(), ((LinkedHashMap<?, ?>)actual).get("set1").toString());
assertEquals(expectedToList.toString(), ((LinkedHashMap<?, ?>)actual).get("tolist").toString());
}
@Test @Test
public void arraysWillCauseArrayIndexOutOfBoundsException_Test() throws ExecutionException, InterruptedException { public void arraysWillCauseArrayIndexOutOfBoundsException_Test() throws ExecutionException, InterruptedException {
msgStr = """ msgStr = """
@ -2399,25 +2679,19 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
list.add(0x35); list.add(0x35);
return isList(list); return isList(list);
"""); """);
}
@Test
public void isSet_Test() throws ExecutionException, InterruptedException {
msgStr = """
{"list": ["C", "B", "A", 34567, "B", "C", "hello", 34]}
""";
decoderStr = """
return isSet(toSet(msg.list)); // return true
""";
Object actual = invokeScript(evalScript(decoderStr), msgStr); Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual); assertInstanceOf(Boolean.class, actual);
assertTrue((Boolean) actual); assertTrue((Boolean) actual);
decoderStr = String.format("""
var list = [];
list.add(0x35);
return isMap(list);
""");
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
decoderStr = String.format("""
var list = [];
list.add(0x35);
return isArray(list);
""");
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
} }
@Test @Test
@ -2435,16 +2709,6 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest {
Object actual = invokeScript(evalScript(decoderStr), msgStr); Object actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual); assertInstanceOf(Boolean.class, actual);
assertTrue((Boolean) actual); assertTrue((Boolean) actual);
decoderStr = """
var array = new int[3];
array[0] = 1;
array[1] = 2;
array[2] = 3;
return isList(array);
""";
actual = invokeScript(evalScript(decoderStr), msgStr);
assertInstanceOf(Boolean.class, actual);
assertFalse((Boolean) actual);
} }
@Test @Test

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

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

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

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

5
application/src/test/java/org/thingsboard/server/transport/coap/attributes/AbstractCoapAttributesIntegrationTest.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeCo
import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
@ -170,7 +171,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
protected void processJsonTestRequestAttributesValuesFromTheServer() throws Exception { protected void processJsonTestRequestAttributesValuesFromTheServer() throws Exception {
client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES);
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(savedDevice.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId()));
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(",")); List<String> clientKeysList = List.of(clientKeysStr.split(","));
@ -200,7 +201,7 @@ public abstract class AbstractCoapAttributesIntegrationTest extends AbstractCoap
protected void processProtoTestRequestAttributesValuesFromTheServer() throws Exception { protected void processProtoTestRequestAttributesValuesFromTheServer() throws Exception {
client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES); client = new CoapTestClient(accessToken, FeatureType.ATTRIBUTES);
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(savedDevice.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId()));
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(",")); List<String> clientKeysList = List.of(clientKeysStr.split(","));

3
application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java

@ -29,6 +29,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter;
@ -161,7 +162,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest {
protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception { protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception {
client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable);
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(savedDevice.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId()));
List<EntityKey> csKeys = getEntityKeys(CLIENT_ATTRIBUTE); List<EntityKey> csKeys = getEntityKeys(CLIENT_ATTRIBUTE);
List<EntityKey> shKeys = getEntityKeys(SHARED_ATTRIBUTE); List<EntityKey> shKeys = getEntityKeys(SHARED_ATTRIBUTE);
List<EntityKey> keys = new ArrayList<>(); List<EntityKey> keys = new ArrayList<>();

102
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -21,8 +21,14 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.eclipse.leshan.client.LeshanClient;
import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.servers.LwM2mServer;
import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.ResponseCode;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.response.ErrorCallback;
import org.eclipse.leshan.core.response.ResponseCallback;
import org.eclipse.leshan.core.response.SendResponse;
import org.eclipse.leshan.server.registration.Registration; import org.eclipse.leshan.server.registration.Registration;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
@ -57,6 +63,7 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.Abstrac
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -73,6 +80,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
@ -82,6 +90,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -93,6 +102,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -340,7 +350,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter(); SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId()); sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
LatestValueCmd latestCmd = new LatestValueCmd(); LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel"))); latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
@ -351,7 +361,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
getWsClient().waitForReply(); getWsClient().waitForReply();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString()); this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), null);
awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr());
String msg = getWsClient().waitForUpdate(); String msg = getWsClient().waitForUpdate();
@ -407,7 +417,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter(); SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(device.getId()); sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
LatestValueCmd latestCmd = new LatestValueCmd(); LatestValueCmd latestCmd = new LatestValueCmd();
String key1 = "pkgname"; String key1 = "pkgname";
String key2 = "pkgversion"; String key2 = "pkgversion";
@ -422,7 +432,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
getWsClient().waitForReply(); getWsClient().waitForReply();
getWsClient().registerWaitForUpdate(); getWsClient().registerWaitForUpdate();
this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString()); this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString(), null);
awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr()); awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr());
String msg = getWsClient().waitForUpdate(); String msg = getWsClient().waitForUpdate();
@ -543,16 +553,17 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, String deviceIdStr) throws Exception { String endpoint, String deviceIdStr) throws Exception {
this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr); this.createNewClient(security, securityBs, isRpc, endpoint, null, false, deviceIdStr, null);
} }
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength, String deviceIdStr) throws Exception { String endpoint, Integer clientDtlsCidLength, String deviceIdStr) throws Exception {
this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr); this.createNewClient(security, securityBs, isRpc, endpoint, clientDtlsCidLength, false, deviceIdStr, null);
} }
public void createNewClient(Security security, Security securityBs, boolean isRpc, public void createNewClient(Security security, Security securityBs, boolean isRpc,
String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception { String endpoint, Integer clientDtlsCidLength, boolean queueMode,
String deviceIdStr, Integer value3_0_9) throws Exception {
this.clientDestroy(false); this.clientDestroy(false);
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources);
@ -560,11 +571,86 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
int clientPort = socket.getLocalPort(); int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, securityBs, clientPort, isRpc, lwM2MTestClient.init(security, securityBs, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest, this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest,
clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR); clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR, value3_0_9);
} }
lwM2MTestClient.setDeviceIdStr(deviceIdStr); lwM2MTestClient.setDeviceIdStr(deviceIdStr);
} }
/**
* Test: "/3/0/9" value = 44 (constant); count = 10; send from client to telemetry without observe
* @param security
* @param deviceCredentials
* @param endpoint
* @param queueMode
* @throws Exception
*/
public void testConnectionWithoutObserveWithDataReceivedSingleTelemetry(Security security,
LwM2MDeviceCredentials deviceCredentials,
String endpoint,
boolean queueMode) throws Exception {
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE));
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration);
Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId());
SingleEntityFilter sef = new SingleEntityFilter();
sef.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
LatestValueCmd latestCmd = new LatestValueCmd();
latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, "batteryLevel")));
EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
EntityDataCmd cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
getWsClient().send(cmd);
getWsClient().waitForReply();
getWsClient().registerWaitForUpdate();
this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString(), 44);
awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr());
LeshanClient leshanClient = lwM2MTestClient.getLeshanClient();
Map<String, LwM2mServer> registeredServers = leshanClient.getRegisteredServers();
List<String> paths = List.of("/3/0/9");
int cntUpdate = 10;
int cntLast = cntUpdate;
for (final LwM2mServer server : registeredServers.values()) {
log.info("Sending Data to {} using {}.", server, ContentFormat.SENML_CBOR);
ResponseCallback<SendResponse> responseCallback = (response) -> {
if (response.isSuccess())
log.warn("Data sent successfully to {} [{}].", server, response.getCode());
else
log.warn("Send data to {} failed [{}] : {}.", server, response.getCode(),
response.getErrorMessage() == null ? "" : response.getErrorMessage());
};
ErrorCallback errorCallback = (e) -> log.warn("Unable to send data to {}.", server, e);
while(cntLast > 0) {
leshanClient.getSendService().sendData(server, ContentFormat.SENML_CBOR, paths,
2000, responseCallback, errorCallback);
cntLast-- ;
}
}
verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate))
.updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null));
String msg = getWsClient().waitForUpdate();
EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class);
Assert.assertEquals(1, update.getCmdId());
List<EntityData> eData = update.getUpdate();
Assert.assertNotNull(eData);
Assert.assertEquals(1, eData.size());
Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel");
assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class));
int expected = 44;
assertEquals(expected, Long.parseLong(tsValue.getValue()));
}
private void clientDestroy(boolean isAfter) { private void clientDestroy(boolean isAfter) {
try { try {
if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) {

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -144,7 +144,7 @@ public class LwM2MTestClient {
public void init(Security security, Security securityBs, int port, boolean isRpc, public void init(Security security, Security securityBs, int port, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode, LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR) throws InvalidDDFFileException, IOException { boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient); Assert.assertNull("client already initialized", leshanClient);
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext; this.clientContext = clientContext;
@ -197,7 +197,7 @@ public class LwM2MTestClient {
initializer.setInstancesForObject(SERVER, lwm2mServer); initializer.setInstancesForObject(SERVER, lwm2mServer);
} }
initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor)); initializer.setInstancesForObject(DEVICE, lwM2MDevice = new SimpleLwM2MDevice(executor, value3_0_9));
initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice());
initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice());
initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class);

26
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java

@ -85,18 +85,22 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
*/ */
private static Map<Integer, Long> errorCode = private static Map<Integer, Long> errorCode =
Map.of(0, 0L); // 0-32 Map.of(0, 0L); // 0-32
private Integer value3_0_9;
public SimpleLwM2MDevice() { public SimpleLwM2MDevice() {
} }
public SimpleLwM2MDevice(ScheduledExecutorService executorService) { public SimpleLwM2MDevice(ScheduledExecutorService executorService, Integer value3_0_9) {
this.value3_0_9 = value3_0_9;
try { try {
executorService.scheduleWithFixedDelay(() -> { if ( this.value3_0_9 == null) {
fireResourceChange(9); executorService.scheduleWithFixedDelay(() -> {
fireResourceChange(20); fireResourceChange(9);
} fireResourceChange(20);
, 1, 1, TimeUnit.SECONDS); // 2 sec }
, 1, 1, TimeUnit.SECONDS); // 2 sec
// , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN // , 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN
}
} catch (Throwable e) { } catch (Throwable e) {
log.error("[{}]Throwable", e.toString()); log.error("[{}]Throwable", e.toString());
e.printStackTrace(); e.printStackTrace();
@ -211,8 +215,14 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl
} }
private int getBatteryLevel() { private int getBatteryLevel() {
int valBattery = randomIterator.nextInt(); int valBattery;
log.trace("Send from client [3/0/9] val: [{}]", valBattery); if (this.value3_0_9 == null) {
valBattery = randomIterator.nextInt();
log.trace("Send from client [3/0/9] val: [{}]", valBattery);
} else {
valBattery = this.value3_0_9;
log.warn("Send from client [3/0/9] constant value: [{}]", valBattery);
}
return valBattery; return valBattery;
} }

31
application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDataReceivedFromClientTest.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.lwm2m.rpc.sql;
import org.junit.Test;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest;
public class RpcLwm2mIntegrationDataReceivedFromClientTest extends AbstractSecurityLwM2MIntegrationTest {
@Test
public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception {
String clientEndpoint = CLIENT_ENDPOINT_NO_SEC;
LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint));
super.testConnectionWithoutObserveWithDataReceivedSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false);
}
}

7
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AliasEntityId;
import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
@ -339,7 +340,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
MqttTestClient client = new MqttTestClient(); MqttTestClient client = new MqttTestClient();
client.connectAndWait(accessToken); client.connectAndWait(accessToken);
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(savedDevice.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(savedDevice.getId()));
String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson"; String clientKeysStr = "clientStr,clientBool,clientDbl,clientLong,clientJson";
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(",")); List<String> clientKeysList = List.of(clientKeysStr.split(","));
@ -426,7 +427,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
}); });
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(device.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> clientKeysList = List.of(clientKeysStr.split(",")); List<String> clientKeysList = List.of(clientKeysStr.split(","));
List<String> sharedKeysList = List.of(sharedKeysStr.split(",")); List<String> sharedKeysList = List.of(sharedKeysStr.split(","));
@ -481,7 +482,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt
assertNotNull(device); assertNotNull(device);
SingleEntityFilter dtf = new SingleEntityFilter(); SingleEntityFilter dtf = new SingleEntityFilter();
dtf.setSingleEntity(device.getId()); dtf.setSingleEntity(AliasEntityId.fromEntityId(device.getId()));
String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson"; String sharedKeysStr = "sharedStr,sharedBool,sharedDbl,sharedLong,sharedJson";
List<String> sharedKeysList = List.of(sharedKeysStr.split(",")); List<String> sharedKeysList = List.of(sharedKeysStr.split(","));
List<EntityKey> csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE); List<EntityKey> csKeys = getEntityKeys(clientKeysList, CLIENT_ATTRIBUTE);

103
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java

@ -29,6 +29,7 @@ import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode;
import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage;
import org.junit.Assert; import org.junit.Assert;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@ -67,7 +68,7 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataTyp
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0;
/** /**
* Created by nickAS21 on 12.01.23 * Created by nickAS21 on 12.01.23
@ -112,31 +113,44 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
long value = bdSeq = 0; long value = bdSeq = 0;
clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); clientWithCorrectNodeAccessTokenWithNDEATH(ts, value);
} }
public void clientWithCorrectNodeAccessTokenWithNDEATH(Long alias) throws Exception {
long ts = calendar.getTimeInMillis();
long value = bdSeq = 0;
clientMqttV5ConnectWithNDEATH(ts, value,alias);
}
public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception {
IMqttToken connectionResult = clientConnectWithNDEATH(ts, value); IMqttToken connectionResult = clientMqttV5ConnectWithNDEATH(ts, value, -1L);
MqttWireMessage response = connectionResult.getResponse(); MqttWireMessage response = connectionResult.getResponse();
Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType());
MqttConnAck connAckMsg = (MqttConnAck) response; MqttConnAck connAckMsg = (MqttConnAck) response;
Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode());
} }
public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, Long alias, String... nameSpaceBad) throws Exception {
String key = keysBdSeq; return clientMqttV5ConnectWithNDEATH(ts, value, null, alias, nameSpaceBad);
}
public IMqttToken clientMqttV5ConnectWithNDEATH(long ts, long value, String metricName, Long alias, String... nameSpaceBad) throws Exception {
String key = metricName == null ? keysBdSeq : metricName;
MetricDataType metricDataType = Int64; MetricDataType metricDataType = Int64;
SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder()
.setTimestamp(calendar.getTimeInMillis()); .setTimestamp(calendar.getTimeInMillis());
deathPayload.addMetrics(createMetric(value, ts, key, metricDataType)); deathPayload.addMetrics(createMetric(value, ts, key, metricDataType, alias));
byte[] deathBytes = deathPayload.build().toByteArray(); byte[] deathBytes = deathPayload.build().toByteArray();
this.client = new MqttV5TestClient(); this.client = new MqttV5TestClient();
this.mqttCallback = new SparkplugMqttCallback(); this.mqttCallback = new SparkplugMqttCallback();
this.client.setCallback(this.mqttCallback); this.client.setCallback(this.mqttCallback);
MqttConnectionOptions options = new MqttConnectionOptions(); MqttConnectionOptions options = new MqttConnectionOptions();
// If the MQTT client is using MQTT v5.0, the Edge Node’s MQTT CONNECT packet MUST set the Clean Start flag to true and the Session Expiry Interval to 0
options.setCleanStart(true);
options.setSessionExpiryInterval(0L);
options.setUserName(gatewayAccessToken); options.setUserName(gatewayAccessToken);
String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0]; String nameSpace = nameSpaceBad.length == 0 ? TOPIC_ROOT_SPB_V_1_0 : nameSpaceBad[0];
String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode;
// The NDEATH message MUST set the MQTT Will QoS to 1 and Retained flag to false
MqttMessage msg = new MqttMessage(); MqttMessage msg = new MqttMessage();
msg.setId(0); msg.setId(0);
msg.setQos(1);
msg.setPayload(deathBytes); msg.setPayload(deathBytes);
options.setWill(topic, msg); options.setWill(topic, msg);
return client.connect(options); return client.connect(options);
@ -148,19 +162,19 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
MetricDataType metricDataType = Int32; MetricDataType metricDataType = Int32;
String key = "Node Metric int32"; String key = "Node Metric int32";
int valueDeviceInt32 = 1024; int valueDeviceInt32 = 1024;
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType); SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, -1L);
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts) .setTimestamp(ts)
.setSeq(getBdSeqNum()); .setSeq(getBdSeqNum());
payloadBirthNode.addMetrics(metric); payloadBirthNode.addMetrics(metric);
payloadBirthNode.setTimestamp(ts); payloadBirthNode.setTimestamp(ts);
if (client.isConnected()) { if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
payloadBirthNode.build().toByteArray(), 0, false); payloadBirthNode.build().toByteArray(), 0, false);
} }
valueDeviceInt32 = 4024; valueDeviceInt32 = 4024;
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32); metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, -1L);
for (int i = 0; i < cntDevices; i++) { for (int i = 0; i < cntDevices; i++) {
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts) .setTimestamp(ts)
@ -169,7 +183,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
payloadBirthDevice.addMetrics(metric); payloadBirthDevice.addMetrics(metric);
if (client.isConnected()) { if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName,
payloadBirthDevice.build().toByteArray(), 0, false); payloadBirthDevice.build().toByteArray(), 0, false);
AtomicReference<Device> device = new AtomicReference<>(); AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created") await(alias + "find device [" + deviceName + "] after created")
@ -187,6 +201,49 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
return devices; return devices;
} }
protected List<Device> connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(long ts) throws Exception {
List<Device> devices = new ArrayList<>();
Long alias = 0L;
clientWithCorrectNodeAccessTokenWithNDEATH(alias++);
MetricDataType metricDataType = Int32;
String key = "Node Metric int32";
int valueDeviceInt32 = 1024;
SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType, alias++);
SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getBdSeqNum());
payloadBirthNode.addMetrics(metric);
payloadBirthNode.setTimestamp(ts);
if (client.isConnected()) {
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
payloadBirthNode.build().toByteArray(), 0, false);
}
valueDeviceInt32 = 4024;
metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32, alias++);
SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder()
.setTimestamp(ts)
.setSeq(getSeqNum());
String deviceName = deviceId + "_" + 1;
payloadBirthDevice.addMetrics(metric);
if (client.isConnected()) {
client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName,
payloadBirthDevice.build().toByteArray(), 0, false);
AtomicReference<Device> device = new AtomicReference<>();
await(alias + "find device [" + deviceName + "] after created")
.atMost(200, TimeUnit.SECONDS)
.until(() -> {
device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class));
return device.get() != null;
});
devices.add(device.get());
}
Assert.assertEquals(1, devices.size());
return devices;
}
protected long getBdSeqNum() throws Exception { protected long getBdSeqNum() throws Exception {
if (bdSeq == 256) { if (bdSeq == 256) {
bdSeq = 0; bdSeq = 0;
@ -207,16 +264,20 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
.setTimestamp(calendar.getTimeInMillis()); .setTimestamp(calendar.getTimeInMillis());
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long valueBdSec = getBdSeqNum(); long valueBdSec = getBdSeqNum();
payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64)); payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64, -1L));
listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq); listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq);
payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean)); payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean, -1L));
listKeys.add(keyNodeRebirth); listKeys.add(keyNodeRebirth);
payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType)); if (StringUtils.isNotBlank(metricKey)) {
payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType, -1L));
} else {
payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType, 4L));
}
listKeys.add(metricKey); listKeys.add(metricKey);
if (client.isConnected()) { if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode,
payloadBirthNode.build().toByteArray(), 0, false); payloadBirthNode.build().toByteArray(), 0, false);
} }
return listKeys; return listKeys;
@ -297,7 +358,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value, private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value,
long ts, MetricDataType metricDataType) throws ThingsboardException { long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value)))); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value))));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L));
return tsKvEntry; return tsKvEntry;
} }
@ -305,7 +366,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
long ts, MetricDataType metricDataType) throws ThingsboardException { long ts, MetricDataType metricDataType) throws ThingsboardException {
Double dd = Double.parseDouble(Float.toString(value)); Double dd = Double.parseDouble(Float.toString(value));
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd)); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L));
return tsKvEntry; return tsKvEntry;
} }
@ -313,21 +374,21 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
long ts, MetricDataType metricDataType) throws ThingsboardException { long ts, MetricDataType metricDataType) throws ThingsboardException {
Long l = Double.valueOf(value).longValue(); Long l = Double.valueOf(value).longValue();
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l)); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L));
return tsKvEntry; return tsKvEntry;
} }
private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value, private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value,
long ts, MetricDataType metricDataType) throws ThingsboardException { long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value)); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L));
return tsKvEntry; return tsKvEntry;
} }
private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value, private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value,
long ts, MetricDataType metricDataType) throws ThingsboardException { long ts, MetricDataType metricDataType) throws ThingsboardException {
TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value)); TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value));
dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(value, ts, key, metricDataType, -1L));
return tsKvEntry; return tsKvEntry;
} }
@ -348,7 +409,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
if (nodeArray.size() > 0) { if (nodeArray.size() > 0) {
Optional<TsKvEntry> tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString()))); Optional<TsKvEntry> tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString())));
if (tsKvEntryOptional.isPresent()) { if (tsKvEntryOptional.isPresent()) {
dataPayload.addMetrics(createMetric(values, ts, key, metricDataType)); dataPayload.addMetrics(createMetric(values, ts, key, metricDataType, -1L));
listTsKvEntry.add(tsKvEntryOptional.get()); listTsKvEntry.add(tsKvEntryOptional.get());
listKeys.add(key); listKeys.add(key);
} }
@ -416,7 +477,7 @@ public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttInte
return java.util.UUID.randomUUID().toString(); return java.util.UUID.randomUUID().toString();
} }
public class SparkplugMqttCallback implements MqttCallback { public class SparkplugMqttCallback implements MqttCallback {
private final List<SparkplugBProto.Payload.Metric> messageArrivedMetrics = new ArrayList<>(); private final List<SparkplugBProto.Payload.Metric> messageArrivedMetrics = new ArrayList<>();
@Override @Override

73
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.transport.mqtt.sparkplug.attributes; package org.thingsboard.server.transport.mqtt.sparkplug.attributes;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -33,8 +32,10 @@ import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DDATA;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NDATA;
/** /**
* Created by nickAS21 on 12.01.23 * Created by nickAS21 on 12.01.23
@ -50,7 +51,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Assert.assertTrue(listKeys.contains(keyNodeRebirth)); Assert.assertTrue(listKeys.contains(keyNodeRebirth));
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}"; String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}";
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + SparkplugMessageType.NBIRTH.name()) await(alias + SparkplugMessageType.NBIRTH.name())
@ -76,7 +76,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Object metricValue = nextBoolean(); Object metricValue = nextBoolean();
connectionWithNBirth(metricDataType, metricKey, metricValue); connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
// Boolean <-> String // Boolean <-> String
@ -140,7 +139,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Object metricValue = nextUInt32(); Object metricValue = nextUInt32();
connectionWithNBirth(metricDataType, metricKey, metricValue); connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
// Long <-> String // Long <-> String
@ -192,7 +190,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Object metricValue = nextFloat(30, 400); Object metricValue = nextFloat(30, 400);
connectionWithNBirth(metricDataType, metricKey, metricValue); connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
// Float <-> String // Float <-> String
@ -237,6 +234,67 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue());
} }
protected void processClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.ATTRIBUTES, 1);
// Node Edge
SparkplugMessageType messageType = NCMD;
String keyBirthNameNode = "Node Metric int32";
int valueBirthNameNode = 1024;
String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameNode + "\":" + valueBirthNameNode + "}}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + NCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(keyBirthNameNode, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(valueBirthNameNode == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
messageType = NDATA;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameNode + "\":" + valueBirthNameNode + "}}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + NCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(keyBirthNameNode, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(valueBirthNameNode == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
// Device
messageType = DCMD;
String keyBirthNameDevice = metricBirthName_Int32;
int valueBirthNameDevice = 123456;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameDevice + "\":" + valueBirthNameDevice + "}}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + DCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(keyBirthNameDevice, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(valueBirthNameDevice == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
messageType = DDATA;
SHARED_ATTRIBUTES_PAYLOAD = "{\"" + messageType.name() + "\": {\"" + keyBirthNameDevice + "\":" + valueBirthNameDevice + "}}";
doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
await(alias + DCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(keyBirthNameDevice, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertTrue(valueBirthNameDevice == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
mqttCallback.deleteMessageArrivedMetrics(0);
}
protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH(); clientWithCorrectNodeAccessTokenWithNDEATH();
MetricDataType metricDataType = MetricDataType.Double; MetricDataType metricDataType = MetricDataType.Double;
@ -244,7 +302,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Object metricValue = nextDouble(); Object metricValue = nextDouble();
connectionWithNBirth(metricDataType, metricKey, metricValue); connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
// Double <-> String // Double <-> String
@ -296,7 +353,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
Object metricValue = nextString(); Object metricValue = nextString();
connectionWithNBirth(metricDataType, metricKey, metricValue); connectionWithNBirth(metricDataType, metricKey, metricValue);
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.ATTRIBUTES, 1);
// String <-> Long // String <-> Long
@ -356,7 +412,6 @@ public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends Abstra
return mqttCallback.getMessageArrivedMetrics().size() == 1; return mqttCallback.getMessageArrivedMetrics().size() == 1;
}); });
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName());
Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
} }

4
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java

@ -57,6 +57,10 @@ public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSp
public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk(); processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk();
} }
@Test
public void testClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk() throws Exception {
processClientWithCorrectAccessTokenPublishMetricDataTypeFromJson_SendValueOk();
}
@Test @Test
public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception {

6
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java

@ -40,7 +40,7 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0;
/** /**
* Created by nickAS21 on 12.01.23 * Created by nickAS21 on 12.01.23
@ -80,7 +80,7 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra
protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception {
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long value = bdSeq = 0; long value = bdSeq = 0;
MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2")); MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, -1L,"spBv1.2"));
String expectedMessage = "Server unavailable."; String expectedMessage = "Server unavailable.";
int expectedReasonCode = 136; int expectedReasonCode = 136;
Assert.assertEquals(expectedMessage, actualException.getMessage()); Assert.assertEquals(expectedMessage, actualException.getMessage());
@ -135,7 +135,7 @@ public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends Abstra
if (client.isConnected()) { if (client.isConnected()) {
List<Device> devicesList = new ArrayList<>(devices); List<Device> devicesList = new ArrayList<>(devices);
Device device = devicesList.get(indexDeviceDisconnect); Device device = devicesList.get(indexDeviceDisconnect);
client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(),
payloadDeathDevice.build().toByteArray(), 0, false); payloadDeathDevice.build().toByteArray(), 0, false);
await(alias + messageName(STATE) + ", device: " + device.getName()) await(alias + messageName(STATE) + ", device: " + device.getName())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)

39
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java

@ -15,8 +15,8 @@
*/ */
package org.thingsboard.server.transport.mqtt.sparkplug.rpc; package org.thingsboard.server.transport.mqtt.sparkplug.rpc;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.common.MqttException;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -32,7 +32,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS; import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE;
@Slf4j @Slf4j
public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest { public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest {
@ -45,7 +44,6 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie
clientWithCorrectNodeAccessTokenWithNDEATH(); clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1);
String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}"; String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}";
String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway); String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway);
@ -63,9 +61,10 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie
public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
long ts = calendar.getTimeInMillis(); long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts);
awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.RPC, 1);
String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}";
String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0));
await(alias + NCMD.name()) await(alias + DCMD.name())
.atMost(40, TimeUnit.SECONDS) .atMost(40, TimeUnit.SECONDS)
.until(() -> { .until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1; return mqttCallback.getMessageArrivedMetrics().size() == 1;
@ -75,12 +74,41 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
} }
@Test
public void processClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success() throws Exception {
long ts = calendar.getTimeInMillis();
List<Device> devices = connectClientWithCorrectAccessTokenWithNDEATHWithAliasCreatedDevices(ts);
awaitForDeviceActorToReceiveSubscription(devices.get(0).getId(), FeatureType.RPC, 1);
String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}";
String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0));
await(alias + DCMD.name())
.atMost(40, TimeUnit.SECONDS)
.until(() -> {
return mqttCallback.getMessageArrivedMetrics().size() == 1;
});
Assert.assertEquals(expected, actual);
Assert.assertFalse(mqttCallback.getMessageArrivedMetrics().get(0).hasName());
Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).hasAlias());
Assert.assertTrue(2L == mqttCallback.getMessageArrivedMetrics().get(0).getAlias());
Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue());
}
@Test
public void processClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS() throws Exception {
long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS;
long value = bdSeq = 0;
MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientMqttV5ConnectWithNDEATH(ts, value, "",4L));
String expectedMessage = "Server unavailable.";
int expectedReasonCode = 136;
Assert.assertEquals(expectedMessage, actualException.getMessage());
Assert.assertEquals(expectedReasonCode, actualException.getReasonCode());
}
@Test @Test
public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception {
clientWithCorrectNodeAccessTokenWithNDEATH(); clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1);
String invalidateTypeMessageName = "RCMD"; String invalidateTypeMessageName = "RCMD";
String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " + String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " +
@ -94,7 +122,6 @@ public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5Clie
clientWithCorrectNodeAccessTokenWithNDEATH(); clientWithCorrectNodeAccessTokenWithNDEATH();
connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32());
Assert.assertTrue("Connection node is failed", client.isConnected()); Assert.assertTrue("Connection node is failed", client.isConnected());
client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE);
awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1); awaitForDeviceActorToReceiveSubscription(savedGateway.getId(), FeatureType.RPC, 1);
String metricNameBad = metricBirthName_Int32 + "_Bad"; String metricNameBad = metricBirthName_Int32 + "_Bad";
String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}"; String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}";

9
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java

@ -47,6 +47,15 @@ public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest {
public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception {
processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success(); processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success();
} }
@Test
public void testClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success() throws Exception {
processClientDeviceWithCorrectAccessTokenPublishWithAlias_TwoWayRpc_Success();
}
@Test
public void testClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS() throws Exception {
processClientNodeWithCorrectAccessTokenPublishWithAliasWithoutMetricName_TwoWayRpc_BAD_REQUEST_PARAMS();
}
@Test @Test
public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception {

6
application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java

@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_ROOT_SPB_V_1_0;
/** /**
* Created by nickAS21 on 12.01.23 * Created by nickAS21 on 12.01.23
@ -67,7 +67,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac
createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts);
if (client.isConnected()) { if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + messageTypeName + "/" + edgeNode,
ndataPayload.build().toByteArray(), 0, false); ndataPayload.build().toByteArray(), 0, false);
} }
@ -96,7 +96,7 @@ public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends Abstrac
createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts);
if (client.isConnected()) { if (client.isConnected()) {
client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, client.publish(TOPIC_ROOT_SPB_V_1_0 + "/" + groupId + "/" + messageTypeName + "/" + edgeNode,
ndataPayload.build().toByteArray(), 0, false); ndataPayload.build().toByteArray(), 0, false);
} }

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

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

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

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

5
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java

@ -17,10 +17,11 @@ package org.thingsboard.server.coapserver;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@Inherited
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true')") @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true')")
public @interface TbCoapServerComponent { public @interface TbCoapServerComponent {}
}

5
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java

@ -17,11 +17,12 @@ package org.thingsboard.server.coapserver;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@Inherited
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || " + @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || " +
"('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true' && '${transport.coap.enabled}'=='true')") "('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true' && '${transport.coap.enabled}'=='true')")
public @interface TbCoapTransportComponent { public @interface TbCoapTransportComponent {}
}

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

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

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.ai;
import com.google.common.util.concurrent.FluentFuture;
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;
import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Optional;
public interface AiModelService extends EntityDaoService {
AiModel save(AiModel model);
Optional<AiModel> findAiModelById(TenantId tenantId, AiModelId modelId);
PageData<AiModel> findAiModelsByTenantId(TenantId tenantId, PageLink pageLink);
Optional<AiModel> findAiModelByTenantIdAndId(TenantId tenantId, AiModelId modelId);
FluentFuture<Optional<AiModel>> findAiModelByTenantIdAndIdAsync(TenantId tenantId, AiModelId modelId);
boolean deleteByTenantIdAndId(TenantId tenantId, AiModelId modelId);
}

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

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.alarm; package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
@ -105,6 +106,8 @@ public interface AlarmService extends EntityDaoService {
Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
FluentFuture<Alarm> findLatestActiveByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds); AlarmDataQuery query, Collection<EntityId> orderedEntityIds);

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

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

5
common/dao-api/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsService.java

@ -18,8 +18,9 @@ package org.thingsboard.server.dao.settings;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.AdminSettingsId; import org.thingsboard.server.common.data.id.AdminSettingsId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.entity.EntityDaoService;
public interface AdminSettingsService { public interface AdminSettingsService extends EntityDaoService {
AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId); AdminSettings findAdminSettingsById(TenantId tenantId, AdminSettingsId adminSettingsId);
@ -31,6 +32,4 @@ public interface AdminSettingsService {
boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key); boolean deleteAdminSettingsByTenantIdAndKey(TenantId tenantId, String key);
void deleteAdminSettingsByTenantId(TenantId tenantId);
} }

6
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>4.1.0-RC</version> <version>4.2.0-RC</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>
@ -112,6 +112,10 @@
<artifactId>leshan-core</artifactId> <artifactId>leshan-core</artifactId>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

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

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
@ -41,6 +42,11 @@ public abstract class BaseData<I extends UUIDBased> extends IdBased<I> implement
this.createdTime = data.getCreatedTime(); this.createdTime = data.getCreatedTime();
} }
@Schema(
description = "Entity creation timestamp in milliseconds since Unix epoch",
example = "1746028547220",
accessMode = Schema.AccessMode.READ_ONLY
)
public long getCreatedTime() { public long getCreatedTime() {
return createdTime; return createdTime;
} }

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

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.common.data; package org.thingsboard.server.common.data;
public class CacheConstants { public final class CacheConstants {
private CacheConstants() {}
public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
public static final String RELATIONS_CACHE = "relations"; public static final String RELATIONS_CACHE = "relations";
public static final String DEVICE_CACHE = "devices"; public static final String DEVICE_CACHE = "devices";
@ -36,6 +39,7 @@ public class CacheConstants {
public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings"; public static final String NOTIFICATION_SETTINGS_CACHE = "notificationSettings";
public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications";
public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings";
public static final String AI_MODEL_CACHE = "aiModel";
public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ASSET_PROFILE_CACHE = "assetProfiles";
public static final String ATTRIBUTES_CACHE = "attributes"; public static final String ATTRIBUTES_CACHE = "attributes";
@ -54,4 +58,5 @@ public class CacheConstants {
public static final String ALARM_TYPES_CACHE = "alarmTypes"; public static final String ALARM_TYPES_CACHE = "alarmTypes";
public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings"; public static final String QR_CODE_SETTINGS_CACHE = "qrCodeSettings";
public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey"; public static final String MOBILE_SECRET_KEY_CACHE = "mobileSecretKey";
} }

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

Loading…
Cancel
Save