Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/cf-output

pull/14225/head
IrynaMatveieva 7 months ago
parent
commit
dd977782f9
  1. 7
      application/src/main/data/upgrade/basic/schema_update.sql
  2. 23
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  3. 4
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  4. 6
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  5. 80
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  6. 10
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  8. 1
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  9. 9
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  10. 17
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  11. 28
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  12. 60
      application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java
  13. 34
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  14. 31
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  15. 17
      application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java
  16. 14
      common/dao-api/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldService.java
  17. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
  18. 7
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java
  19. 46
      common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java
  20. 17
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java
  21. 12
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java
  22. 12
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java
  23. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java
  24. 45
      common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java
  25. 1
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  26. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  27. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  28. 26
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java
  29. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java
  30. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java
  31. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java
  32. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java
  33. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java
  34. 13
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  35. 2
      common/proto/src/main/proto/queue.proto
  36. 4
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  37. 62
      dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java
  38. 42
      dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java
  39. 4
      dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
  40. 4
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  41. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  42. 4
      dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java
  43. 21
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  44. 3
      dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java
  45. 12
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  46. 79
      dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java
  47. 4
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  48. 41
      dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java
  49. 36
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java
  50. 38
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java
  51. 94
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java
  52. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java
  53. 4
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  54. 10
      dao/src/main/resources/sql/schema-entities.sql
  55. 6
      dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
  56. 62
      dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java
  57. 19
      dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java
  58. 45
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  59. 5
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java
  60. 10
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java
  61. 57
      dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java
  62. 10
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java
  63. 128
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java
  64. 271
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  65. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java
  66. 11
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java
  67. 81
      ui-ngx/src/app/core/http/attribute.service.ts
  68. 31
      ui-ngx/src/app/core/http/http-utils.ts
  69. 2
      ui-ngx/src/app/core/http/queue.service.ts
  70. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  71. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  72. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  73. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
  74. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  75. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  76. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  77. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  78. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  79. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  80. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  81. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  82. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  83. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  84. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  85. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss
  86. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss
  87. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  88. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts
  89. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts
  90. 13
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts
  91. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  92. 2
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  93. 2
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html
  94. 2
      ui-ngx/src/assets/help/en_US/notification/alarm.md
  95. 1
      ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md
  96. 1
      ui-ngx/src/assets/help/en_US/notification/alarm_comment.md
  97. 3
      ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md
  98. 3
      ui-ngx/src/assets/help/en_US/notification/edge_connection.md
  99. 3
      ui-ngx/src/assets/help/en_US/notification/resources_shortage.md
  100. 3
      ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md

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

@ -88,3 +88,10 @@ SET configuration = jsonb_set(
WHERE (configuration::jsonb -> 'output' -> 'strategy') IS NULL;
-- CALCULATED FIELD OUTPUT STRATEGY UPDATE END
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE START
DROP TABLE IF EXISTS calculated_field_link;
ANALYZE calculated_field;
-- REMOVAL OF CALCULATED FIELD LINKS PERSISTENCE END

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

@ -611,7 +611,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var proto = msg.getProto();
List<CalculatedFieldEntityCtxId> result = new ArrayList<>();
for (var link : getCalculatedFieldLinksByEntityId(entityId)) {
CalculatedFieldCtx ctx = calculatedFields.get(link.getCalculatedFieldId());
CalculatedFieldCtx ctx = calculatedFields.get(link.calculatedFieldId());
if (ctx.linkMatches(entityId, proto)) {
result.add(ctx.toCalculatedFieldEntityCtxId());
}
@ -738,13 +738,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
private void addLinks(CalculatedField newCf) {
var newLinks = newCf.getConfiguration().buildCalculatedFieldLinks(tenantId, newCf.getEntityId(), newCf.getId());
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link));
newLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link));
}
private void deleteLinks(CalculatedFieldCtx cfCtx) {
var oldCf = cfCtx.getCalculatedField();
var oldLinks = oldCf.getConfiguration().buildCalculatedFieldLinks(tenantId, oldCf.getEntityId(), oldCf.getId());
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).remove(link));
oldLinks.forEach(link -> entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).remove(link));
}
public void onPartitionChange(CalculatedFieldPartitionChangeMsg msg) {
@ -757,15 +757,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.trace("Processing calculated field record: {}", cf);
try {
initCalculatedField(cf);
initCalculatedFieldLinks(cf);
} catch (CalculatedFieldException e) {
log.error("Failed to process calculated field record: {}", cf, e);
}
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfls.forEach(link -> {
log.trace("Processing calculated field link record: {}", link);
initCalculatedFieldLink(link);
});
}
private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException {
@ -782,10 +778,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
private void initCalculatedFieldLinks(CalculatedField cf) {
List<CalculatedFieldLink> links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId());
for (CalculatedFieldLink link : links) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link);
}
}
private void initEntitiesCache() {

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

@ -63,10 +63,10 @@ import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
@ -287,7 +287,7 @@ public class CalculatedFieldController extends BaseController {
}
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {

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

@ -1630,11 +1630,13 @@ public class ControllerConstants {
protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION;
protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'. " +
"If attribute keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key";
protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details.";
protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest time series. For example, 'temperature,humidity'. " +
"If telemetry keys contain comma, duplicate 'key' parameter for each key, for example '?key=my,key&key=my,second,key";
protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility";
protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details.";

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

@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -208,10 +209,12 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> getAttributes(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keys));
}
@ -231,10 +234,12 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keys));
}
@ApiOperation(value = "Get time series keys (getTimeseriesKeys)",
@ -270,10 +275,12 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes));
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keys, useStrictDataTypes));
}
@ApiOperation(value = "Get time series data (getTimeseries)",
@ -291,7 +298,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> getTimeseries(
@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 = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys,
@Parameter(description = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = "A long value representing the start timestamp of the time range in milliseconds, UTC.")
@RequestParam(name = "startTs") Long startTs,
@Parameter(description = "A long value representing the end timestamp of the time range in milliseconds, UTC.")
@ -312,9 +319,11 @@ public class TelemetryController extends BaseController {
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy,
@Parameter(description = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
DeferredResult<ResponseEntity> response = new DeferredResult<>();
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), toKeysList(keys), startTs, endTs,
Futures.addCallback(tbTelemetryService.getTimeseries(EntityIdFactory.getByTypeAndId(entityType, entityIdStr), keys, startTs, endTs,
intervalType, interval, timeZone, limit, Aggregation.valueOf(aggStr), orderBy, useStrictDataTypes, getCurrentUser()),
getTsKvListCallback(response, useStrictDataTypes), MoreExecutors.directExecutor());
return response;
@ -466,7 +475,7 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> deleteEntityTimeseries(
@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 = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr,
@Parameter(description = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@Parameter(description = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.")
@RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys,
@Parameter(description = "A long value representing the start timestamp of removal time range in milliseconds.")
@ -476,16 +485,17 @@ public class TelemetryController extends BaseController {
@Parameter(description = "If the parameter is set to true, the latest telemetry can be removed, otherwise, in case that parameter is set to false the latest value will not removed.")
@RequestParam(name = "deleteLatest", required = false, defaultValue = "true") boolean deleteLatest,
@Parameter(description = "If the parameter is set to true, the latest telemetry will be rewritten in case that current latest value was removed, otherwise, in case that parameter is set to false the new latest value will not set.")
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException {
@RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
return deleteTimeseries(entityId, keys, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted, deleteLatest);
}
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys,
private DeferredResult<ResponseEntity> deleteTimeseries(EntityId entityIdStr, List<String> keys, boolean deleteAllDataForKeys,
Long startTs, Long endTs, boolean rewriteLatestIfDeleted, boolean deleteLatest) throws ThingsboardException {
List<String> keys = toKeysList(keysStr);
if (keys.isEmpty()) {
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
@ -547,9 +557,11 @@ public class TelemetryController extends BaseController {
public DeferredResult<ResponseEntity> deleteDeviceAttributes(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED)) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return deleteAttributes(entityId, scope, keysStr);
return deleteAttributes(entityId, scope, keys);
}
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
@ -570,15 +582,20 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) @PathVariable("scope") AttributeScope scope,
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
@Parameter(description = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@RequestParam MultiValueMap<String, String> params) throws ThingsboardException {
List<String> keys = getKeys(keysStr, params);
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteAttributes(entityId, scope, keysStr);
return deleteAttributes(entityId, scope, keys);
}
private List<String> getKeys(String keysStr, MultiValueMap<String, String> params) {
return params.get("key") != null ? params.get("key") : toKeysList(keysStr);
}
private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdSrc, AttributeScope scope, String keysStr) throws ThingsboardException {
List<String> keys = toKeysList(keysStr);
private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdSrc, AttributeScope scope, List<String> keys) throws ThingsboardException {
if (keys.isEmpty()) {
return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
return getImmediateDeferredResult("Empty keys: " + keys, HttpStatus.BAD_REQUEST);
}
SecurityUser user = getCurrentUser();
@ -709,30 +726,29 @@ public class TelemetryController extends BaseController {
});
}
private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys, Boolean useStrictDataTypes) {
private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, List<String> keys, Boolean useStrictDataTypes) {
ListenableFuture<List<TsKvEntry>> future;
if (StringUtils.isEmpty(keys)) {
if (keys.isEmpty()) {
future = tsService.findAllLatest(user.getTenantId(), entityId);
} else {
future = tsService.findLatest(user.getTenantId(), entityId, toKeysList(keys));
future = tsService.findLatest(user.getTenantId(), entityId, keys);
}
Futures.addCallback(future, getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
}
private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, AttributeScope scope, String keys) {
List<String> keyList = toKeysList(keys);
FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, AttributeScope scope, List<String> keys) {
FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keys);
if (scope != null) {
if (keyList != null && !keyList.isEmpty()) {
Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keyList), callback, MoreExecutors.directExecutor());
if (keys != null && !keys.isEmpty()) {
Futures.addCallback(attributesService.find(user.getTenantId(), entityId, scope, keys), callback, MoreExecutors.directExecutor());
} else {
Futures.addCallback(attributesService.findAll(user.getTenantId(), entityId, scope), callback, MoreExecutors.directExecutor());
}
} else {
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
for (AttributeScope tmpScope : AttributeScope.values()) {
if (keyList != null && !keyList.isEmpty()) {
futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keyList));
if (keys != null && !keys.isEmpty()) {
futures.add(attributesService.find(user.getTenantId(), entityId, tmpScope, keys));
} else {
futures.add(attributesService.findAll(user.getTenantId(), entityId, tmpScope));
}

10
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -82,19 +82,17 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
cfs.forEach(cf -> {
if (cf != null) {
calculatedFields.putIfAbsent(cf.getId(), cf);
List<CalculatedFieldLink> links = cf.getConfiguration().buildCalculatedFieldLinks(cf.getTenantId(), cf.getEntityId(), cf.getId());
calculatedFieldLinks.put(cf.getId(), new CopyOnWriteArrayList<>(links));
}
});
calculatedFields.values().forEach(cf -> {
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf);
});
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
cfls.forEach(link -> {
calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link);
});
calculatedFieldLinks.values().stream()
.flatMap(List::stream)
.forEach(link ->
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link)
entityIdCalculatedFieldLinks.computeIfAbsent(link.entityId(), id -> new CopyOnWriteArrayList<>()).add(link)
);
}
@ -226,7 +224,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
log.debug("[{}] evict calculated field links from cache: {}", calculatedFieldId, oldCalculatedField);
calculatedFieldsCtx.remove(calculatedFieldId);
log.debug("[{}] evict calculated field ctx from cache: {}", calculatedFieldId, oldCalculatedField);
entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.getCalculatedFieldId().equals(calculatedFieldId)));
entityIdCalculatedFieldLinks.forEach((entityId, calculatedFieldLinks) -> calculatedFieldLinks.removeIf(link -> link.calculatedFieldId().equals(calculatedFieldId)));
log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField);
}

2
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -172,7 +172,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
List<CalculatedFieldLink> links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId);
for (CalculatedFieldLink link : links) {
CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.getCalculatedFieldId());
CalculatedFieldCtx ctx = calculatedFieldCache.getCalculatedFieldCtx(link.calculatedFieldId());
if (ctx != null && linkedEntityFilter.test(ctx)) {
return true;
}

1
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -62,6 +62,7 @@ public class AlarmAssignmentTriggerProcessor implements NotificationRuleTriggerP
.alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmOriginatorLabel(alarmInfo.getOriginatorLabel())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.alarmCustomerId(alarmInfo.getCustomerId())

9
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger;
@ -57,11 +58,14 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(AlarmCommentTrigger trigger) {
Alarm alarm = trigger.getAlarm();
String originatorName;
String originatorName, originatorLabel;
if (alarm instanceof AlarmInfo) {
originatorName = ((AlarmInfo) alarm).getOriginatorName();
originatorLabel = ((AlarmInfo) alarm).getOriginatorLabel();
} else {
originatorName = entityService.fetchEntityName(trigger.getTenantId(), alarm.getOriginator()).orElse("");
var infoOpt = entityService.fetchNameLabelAndCustomerDetails(trigger.getTenantId(), alarm.getOriginator());
originatorName = infoOpt.map(NameLabelAndCustomerDetails::getName).orElse(null);
originatorLabel = infoOpt.map(NameLabelAndCustomerDetails::getLabel).orElse(null);
}
return AlarmCommentNotificationInfo.builder()
.comment(trigger.getComment().getComment().get("text").asText())
@ -73,6 +77,7 @@ public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProc
.alarmType(alarm.getType())
.alarmOriginator(alarm.getOriginator())
.alarmOriginatorName(originatorName)
.alarmOriginatorLabel(originatorLabel)
.alarmSeverity(alarm.getSeverity())
.alarmStatus(alarm.getStatus())
.alarmCustomerId(alarm.getCustomerId())

17
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -15,7 +15,9 @@
*/
package org.thingsboard.server.service.notification.rule.trigger;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
@ -28,6 +30,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.HashMap;
import java.util.Map;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@ -106,15 +111,27 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
alarmUpdate.isDeleted() ? "deleted" : null)
.alarmOriginator(alarmInfo.getOriginator())
.alarmOriginatorName(alarmInfo.getOriginatorName())
.alarmOriginatorLabel(alarmInfo.getOriginatorLabel())
.alarmSeverity(alarmInfo.getSeverity())
.alarmStatus(alarmInfo.getStatus())
.acknowledged(alarmInfo.isAcknowledged())
.cleared(alarmInfo.isCleared())
.alarmCustomerId(alarmInfo.getCustomerId())
.dashboardId(alarmInfo.getDashboardId())
.details(toDetailsTemplateMap(alarmInfo.getDetails()))
.build();
}
private Map<String, String> toDetailsTemplateMap(JsonNode details) {
Map<String, String> infoMap = JacksonUtil.toFlatMap(details);
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, String> entry : infoMap.entrySet()) {
String key = "details." + entry.getKey();
result.put(key, entry.getValue());
}
return result;
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM;

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

@ -210,7 +210,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected static final String TEST_DIFFERENT_TENANT_NAME = "TEST DIFFERENT TENANT";
protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
private static final String SYS_ADMIN_PASSWORD = "sysadmin";
protected static final String SYS_ADMIN_PASSWORD = "sysadmin";
protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
protected static final String TENANT_ADMIN_PASSWORD = "tenant";
@ -686,9 +686,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
}
protected Device createDevice(String name, String accessToken) throws Exception {
return createDevice(name, "default", null, accessToken);
}
protected Device createDevice(String name, String type, String label, String accessToken) throws Exception {
Device device = new Device();
device.setName(name);
device.setType("default");
device.setType(type);
device.setLabel(label);
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration());
deviceData.setConfiguration(new DefaultDeviceConfiguration());
@ -744,6 +749,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return doPost("/api/device-with-credentials", request, Device.class);
}
protected ResultActions doGetAsync(String urlTemplate, MultiValueMap<String, String> params) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate).params(params);
setJwtToken(getRequest);
return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
}
protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
setJwtToken(getRequest);
@ -943,6 +954,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(deleteRequest);
}
protected ResultActions doDeleteAsync(String urlTemplate, MultiValueMap<String, String> params) throws Exception {
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate)
.params(params);
setJwtToken(deleteRequest);
MvcResult result = mockMvc.perform(deleteRequest).andReturn();
result.getAsyncResult(DEFAULT_TIMEOUT);
return mockMvc.perform(asyncDispatch(result));
}
protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception {
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params);
setJwtToken(deleteRequest);
@ -1133,7 +1153,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> {
CalculatedFieldState calculatedFieldState = statesMap.get(cfId);
boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs()
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
< System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval);
log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady);
return isReady;
});
@ -1324,7 +1344,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected List<Job> findJobs(List<JobType> types, List<UUID> entities) throws Exception {
return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) +
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
"&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&",
new TypeReference<PageData<Job>>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData();
}

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

@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
@ -232,6 +235,63 @@ public class TelemetryControllerTest extends AbstractControllerTest {
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody2, String.class, status().isBadRequest());
}
@Test
public void testDeleteTelemetryByKeyWithComma() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String tsKey = "key1,key2";
String testBody = JacksonUtil.newObjectNode()
.put(tsKey, "value")
.toString();
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", tsKey);
params.add("deleteAllDataForKeys", "true");
ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value");
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params);
ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull());
}
@Test
public void testDeleteTelemetryByKeysWithComma() throws Exception {
loginTenantAdmin();
Device device = createDevice();
String keyWithComma = "key1,key2";
String testBody = JacksonUtil.newObjectNode()
.put(keyWithComma, "value")
.toString();
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody, String.class, status().isOk());
String key = "key3";
String testBody2 = JacksonUtil.newObjectNode()
.put(key, "value")
.toString();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", keyWithComma);
params.add("key", key);
params.add("deleteAllDataForKeys", "true");
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", testBody2, String.class, status().isOk());
ObjectNode tsData = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
assertThat(tsData.get("key1,key2").get(0).get("value").asText()).isEqualTo("value");
assertThat(tsData.get("key3").get(0).get("value").asText()).isEqualTo("value");
doDeleteAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/delete", params);
ObjectNode tsDataAfterDeletion = readResponse(doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries", params), ObjectNode.class);
Assert.assertTrue(tsDataAfterDeletion.get("key1,key2").get(0).get("value").isNull());
Assert.assertTrue(tsDataAfterDeletion.get("key3").get(0).get("value").isNull());
}
private Device createDevice() throws Exception {
String testToken = "TEST_TOKEN";

34
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
@ -415,7 +416,8 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType());
TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class);
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String verificationCode = new Totp(secret).now();
@ -433,6 +435,36 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
doGet("/api/user/" + savedDifferentTenantUser.getId()).andExpect(status().isOk());
}
@Test
public void testEnforceTwoFa_sysadmin() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaSettings.setEnforceTwoFa(true);
AllUsersFilter enforcedUsersFilter = new AllUsersFilter();
twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter);
twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
logInWithMfaToken(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD, Authority.MFA_CONFIGURATION_TOKEN);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = doPost("/api/2fa/account/config/generate?providerType=" + totpTwoFaProviderConfig.getProviderType(), TotpTwoFaAccountConfig.class);
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String verificationCode = new Totp(secret).now();
readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class);
JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class);
assertThat(tokenPair.getToken()).isNotEmpty();
assertThat(tokenPair.getRefreshToken()).isNotEmpty();
validateAndSetJwtToken(tokenPair, SYS_ADMIN_EMAIL);
doGet("/api/user/" + user.getId()).andExpect(status().isOk());
}
private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);

31
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -22,9 +22,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.AttributeScope;
@ -134,7 +134,7 @@ import static org.thingsboard.server.common.data.notification.rule.trigger.confi
})
public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@SpyBean
@MockitoSpyBean
private AlarmSubscriptionService alarmSubscriptionService;
@Autowired
private DefaultSystemInfoService systemInfoService;
@ -190,7 +190,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Test
public void testNotificationRuleProcessing_alarmTrigger() throws Exception {
String notificationSubject = "Alarm type: ${alarmType}, status: ${alarmStatus}, " +
"severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}";
"severity: ${alarmSeverity}, deviceId: ${alarmOriginatorId}, details: ${details.data}.";
String notificationText = "Status: ${alarmStatus}, severity: ${alarmSeverity}";
NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.ALARM, notificationSubject, notificationText, NotificationDeliveryMethod.WEB);
@ -218,12 +218,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
clients.put(delay, userAndClient.getSecond());
}
notificationRule.setRecipientsConfig(recipientsConfig);
notificationRule = saveNotificationRule(notificationRule);
saveNotificationRule(notificationRule);
String alarmType = "myBoolIsTrue";
DeviceProfile deviceProfile = createDeviceProfileWithAlarmRules(alarmType);
Device device = createDevice("Device 1", deviceProfile.getName(), "1234");
Device device = createDevice("Device 1", deviceProfile.getName(), "label", "1234");
clients.values().forEach(wsClient -> {
wsClient.subscribeForUnreadNotifications(10).waitForReply(true);
@ -247,7 +247,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0));
assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " +
"severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId());
"severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true.");
assertThat(notification.getText()).isEqualTo("Status: " + AlarmStatus.ACTIVE_UNACK + ", severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase());
assertThat(notification.getType()).isEqualTo(NotificationType.ALARM);
@ -267,7 +267,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
wsClient.waitForUpdate(true);
Notification updatedNotification = wsClient.getLastDataUpdate().getUpdate();
assertThat(updatedNotification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + expectedStatus + ", " +
"severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId());
"severity: " + expectedSeverity.toString().toLowerCase() + ", deviceId: " + device.getId() + ", details: attribute is true.");
assertThat(updatedNotification.getText()).isEqualTo("Status: " + expectedStatus + ", severity: " + expectedSeverity.toString().toLowerCase());
wsClient.close();
@ -293,7 +293,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
List<Notification> notifications = getMyNotifications(false, 10);
assertThat(notifications).singleElement().matches(notification -> {
return notification.getType() == NotificationType.ALARM &&
notification.getSubject().equals("New alarm 'testAlarm'");
notification.getSubject().equals("New alarm 'testAlarm'");
});
});
}
@ -488,11 +488,11 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
assertThat(notifications).anySatisfy(notification -> {
assertThat(notification.getText()).isEqualTo("Rate limits for REST API requests per customer " +
"exceeded for 'Customer'");
"exceeded for 'Customer'");
});
assertThat(notifications).anySatisfy(notification -> {
assertThat(notification.getText()).isEqualTo("Rate limits for notification requests " +
"per rule exceeded for '" + rule.getName() + "'");
"per rule exceeded for '" + rule.getName() + "'");
});
loginSysAdmin();
@ -513,10 +513,10 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.notifyOn(Set.of(ASSIGNED, UNASSIGNED))
.build();
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}'. Assignee: ${assigneeEmail}";
String template = "${userEmail} ${action} alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' with label '${alarmOriginatorLabel}'. Assignee: ${assigneeEmail}";
createNotificationRule(triggerConfig, "Test", template, target.getId());
Device device = createDevice("Device A", "123");
Device device = createDevice("Device A", "default", "test", "123");
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(device.getId())
@ -533,7 +533,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doPost("/api/alarm/" + alarmId + "/assign/" + tenantAdminUserId).andExpect(status().isOk());
}, notification -> {
assertThat(notification.getText()).isEqualTo(
TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A'. Assignee: " + TENANT_ADMIN_EMAIL
TENANT_ADMIN_EMAIL + " assigned alarm on Device 'Device A' with label 'test'. Assignee: " + TENANT_ADMIN_EMAIL
);
});
@ -541,7 +541,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doDelete("/api/alarm/" + alarmId + "/assign").andExpect(status().isOk());
}, notification -> {
assertThat(notification.getText()).isEqualTo(
TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A'. Assignee: "
TENANT_ADMIN_EMAIL + " unassigned alarm on Device 'Device A' with label 'test'. Assignee: "
);
});
}
@ -745,7 +745,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.build();
assertThat(DefaultNotificationDeduplicationService.getDeduplicationKey(expectedTrigger, rule))
.isEqualTo("RATE_LIMITS:TENANT:" + tenantId + ":ENTITY_EXPORT_" +
target.getId() + ":ENTITY_EXPORT,TRANSPORT_MESSAGES_PER_DEVICE");
target.getId() + ":ENTITY_EXPORT,TRANSPORT_MESSAGES_PER_DEVICE");
loginTenantAdmin();
getWsClient().subscribeForUnreadNotifications(10).waitForReply();
@ -952,6 +952,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
configuration.setArguments(Map.of("createAlarm", argument));
AlarmRule alarmRule = new AlarmRule();
alarmRule.setAlarmDetails("attribute is ${createAlarm}");
SimpleAlarmCondition condition = new SimpleAlarmCondition();
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
expression.setExpression("return createAlarm == true;");

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

@ -243,7 +243,7 @@ public class VersionControlTest extends AbstractControllerTest {
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);
Device device = createDevice(null, deviceProfile.getId(), "Device v1.0", "test1", newDevice -> {
Device device = createDevice(deviceProfile.getId(), "Device v1.0", "test1", newDevice -> {
newDevice.setFirmwareId(firmware.getId());
newDevice.setSoftwareId(software.getId());
});
@ -266,7 +266,7 @@ public class VersionControlTest extends AbstractControllerTest {
createVersion("profiles", EntityType.DEVICE_PROFILE);
OtaPackage firmware = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.FIRMWARE);
OtaPackage software = createOtaPackage(tenantId1, deviceProfile.getId(), OtaPackageType.SOFTWARE);
Device device = createDevice(null, deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> {
Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1", newDevice -> {
newDevice.setFirmwareId(firmware.getId());
newDevice.setSoftwareId(software.getId());
});
@ -527,7 +527,7 @@ public class VersionControlTest extends AbstractControllerTest {
@Test
public void testVcWithRelations_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
Device device = createDevice("Device 1", "test1");
EntityRelation relation = createRelation(asset.getId(), device.getId());
String versionId = createVersion("assets and devices", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
@ -553,11 +553,11 @@ public class VersionControlTest extends AbstractControllerTest {
@Test
public void testVcWithRelations_sameTenant() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device1 = createDevice(null, null, "Device 1", "test1");
Device device1 = createDevice("Device 1", "test1");
EntityRelation relation1 = createRelation(device1.getId(), asset.getId());
String versionId = createVersion("assets", EntityType.ASSET);
Device device2 = createDevice(null, null, "Device 2", "test2");
Device device2 = createDevice("Device 2", "test2");
EntityRelation relation2 = createRelation(device2.getId(), asset.getId());
List<EntityRelation> relations = findRelationsByTo(asset.getId());
assertThat(relations).contains(relation1, relation2);
@ -590,7 +590,7 @@ public class VersionControlTest extends AbstractControllerTest {
@Test
public void testVcWithCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
Device device = createDevice("Device 1", "test1");
CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
@ -616,7 +616,7 @@ public class VersionControlTest extends AbstractControllerTest {
@Test
public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
Device device = createDevice("Device 1", "test1");
CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
@ -910,9 +910,8 @@ public class VersionControlTest extends AbstractControllerTest {
login(tenantAdmin2.getEmail(), tenantAdmin2.getEmail());
}
private Device createDevice(CustomerId customerId, DeviceProfileId deviceProfileId, String name, String accessToken, Consumer<Device>... modifiers) {
private Device createDevice(DeviceProfileId deviceProfileId, String name, String accessToken, Consumer<Device>... modifiers) {
Device device = new Device();
device.setCustomerId(customerId);
device.setName(name);
device.setLabel("lbl");
device.setDeviceProfileId(deviceProfileId);

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

@ -16,10 +16,8 @@
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -52,18 +50,6 @@ public interface CalculatedFieldService extends EntityDaoService {
int deleteAllCalculatedFieldsByEntityId(TenantId tenantId, EntityId entityId);
CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink);
CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId);
List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink);
PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink);
boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId);
}

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

@ -23,6 +23,7 @@ import java.util.EnumSet;
import java.util.List;
public enum EntityType {
TENANT(1),
CUSTOMER(2),
USER(3, "tb_user"),
@ -61,7 +62,7 @@ public enum EntityType {
MOBILE_APP(37),
MOBILE_APP_BUNDLE(38),
CALCULATED_FIELD(39),
CALCULATED_FIELD_LINK(40),
// CALCULATED_FIELD_LINK(40), - was removed in 4.3
JOB(41),
ADMIN_SETTINGS(42),
AI_MODEL(43, "ai_model") {

7
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java

@ -21,11 +21,14 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serial;
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema
public class AlarmInfo extends Alarm {
@Serial
private static final long serialVersionUID = 2807343093519543363L;
@Getter
@ -58,8 +61,8 @@ public class AlarmInfo extends Alarm {
public AlarmInfo(AlarmInfo alarmInfo) {
super(alarmInfo);
this.originatorName = alarmInfo.originatorName;
this.originatorLabel = alarmInfo.originatorLabel;
this.originatorName = alarmInfo.getOriginatorName();
this.originatorLabel = alarmInfo.getOriginatorLabel();
this.assignee = alarmInfo.getAssignee();
}

46
common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldLink.java

@ -15,52 +15,8 @@
*/
package org.thingsboard.server.common.data.cf;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedFieldLink extends BaseData<CalculatedFieldLinkId> {
private static final long serialVersionUID = 6492846246722091530L;
private TenantId tenantId;
private EntityId entityId;
@Schema(description = "JSON object with the Calculated Field Id. ", accessMode = Schema.AccessMode.READ_ONLY)
private CalculatedFieldId calculatedFieldId;
public CalculatedFieldLink() {
super();
}
public CalculatedFieldLink(CalculatedFieldLinkId id) {
super(id);
}
public CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {
this.tenantId = tenantId;
this.entityId = entityId;
this.calculatedFieldId = calculatedFieldId;
}
@Override
public String toString() {
return new StringBuilder()
.append("CalculatedFieldLink[")
.append("tenantId=").append(tenantId)
.append(", entityId=").append(entityId)
.append(", calculatedFieldId=").append(calculatedFieldId)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
.toString();
}
}
public record CalculatedFieldLink(TenantId tenantId, EntityId entityId, CalculatedFieldId calculatedFieldId) {}

17
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ArgumentsBasedCalculatedFieldConfiguration.java

@ -19,10 +19,12 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
@ -30,14 +32,15 @@ public interface ArgumentsBasedCalculatedFieldConfiguration extends CalculatedFi
@NotEmpty
Map<String, Argument> getArguments();
default List<EntityId> getReferencedEntities() {
if (getArguments() == null) {
return List.of();
default Set<EntityId> getReferencedEntities() {
var args = getArguments();
if (args == null) {
return Collections.emptySet();
}
return getArguments().values().stream()
return args.values().stream()
.map(Argument::getRefEntityId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
.collect(toSet());
}
}

12
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java

@ -28,7 +28,9 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@JsonTypeInfo(
@ -55,16 +57,12 @@ public interface CalculatedFieldConfiguration {
default void validate() {}
@JsonIgnore
default List<EntityId> getReferencedEntities() {
return List.of();
default Set<EntityId> getReferencedEntities() {
return Collections.emptySet();
}
default CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId) {
CalculatedFieldLink link = new CalculatedFieldLink();
link.setTenantId(tenantId);
link.setEntityId(referencedEntityId);
link.setCalculatedFieldId(calculatedFieldId);
return link;
return new CalculatedFieldLink(tenantId, referencedEntityId, calculatedFieldId);
}
default List<CalculatedFieldLink> buildCalculatedFieldLinks(TenantId tenantId, EntityId cfEntityId, CalculatedFieldId calculatedFieldId) {

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

@ -26,10 +26,13 @@ import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
@Data
public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration {
@ -62,8 +65,11 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
@Override
public List<EntityId> getReferencedEntities() {
return zoneGroups == null ? List.of() : zoneGroups.values().stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList();
public Set<EntityId> getReferencedEntities() {
return zoneGroups == null ? Collections.emptySet() : zoneGroups.values().stream()
.map(ZoneGroupConfiguration::getRefEntityId)
.filter(Objects::nonNull)
.collect(toSet());
}
@Override

4
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java

@ -21,6 +21,7 @@ import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.io.Serializable;
@Schema
@ -28,6 +29,9 @@ import java.io.Serializable;
@Deprecated
public class AlarmRule implements Serializable {
@Serial
private static final long serialVersionUID = -7617427132423304707L;
@Valid
@Schema(description = "JSON object representing the alarm rule condition")
private AlarmCondition condition;

45
common/data/src/main/java/org/thingsboard/server/common/data/id/CalculatedFieldLinkId.java

@ -1,45 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@Schema
public class CalculatedFieldLinkId extends UUIDBased implements EntityId {
private static final long serialVersionUID = 1L;
@JsonCreator
public CalculatedFieldLinkId(@JsonProperty("id") UUID id) {
super(id);
}
public static CalculatedFieldLinkId fromString(String calculatedFieldLinkId) {
return new CalculatedFieldLinkId(UUID.fromString(calculatedFieldLinkId));
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "string", example = "CALCULATED_FIELD_LINK", allowableValues = "CALCULATED_FIELD_LINK")
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD_LINK;
}
}

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

@ -80,7 +80,6 @@ public class EntityIdFactory {
case DOMAIN -> new DomainId(uuid);
case MOBILE_APP_BUNDLE -> new MobileAppBundleId(uuid);
case CALCULATED_FIELD -> new CalculatedFieldId(uuid);
case CALCULATED_FIELD_LINK -> new CalculatedFieldLinkId(uuid);
case JOB -> new JobId(uuid);
case ADMIN_SETTINGS -> new AdminSettingsId(uuid);
case AI_MODEL -> new AiModelId(uuid);

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -53,6 +53,7 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
private UUID alarmId;
private EntityId alarmOriginator;
private String alarmOriginatorName;
private String alarmOriginatorLabel;
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId;
@ -77,7 +78,8 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
"alarmStatus", alarmStatus.toString(),
"alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(),
"alarmOriginatorId", alarmOriginator.getId().toString(),
"alarmOriginatorName", alarmOriginatorName
"alarmOriginatorName", alarmOriginatorName,
"alarmOriginatorLabel", alarmOriginatorLabel
);
}

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -48,6 +48,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
private UUID alarmId;
private EntityId alarmOriginator;
private String alarmOriginatorName;
private String alarmOriginatorLabel;
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private CustomerId alarmCustomerId;
@ -68,7 +69,8 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
"alarmStatus", alarmStatus.toString(),
"alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(),
"alarmOriginatorId", alarmOriginator.getId().toString(),
"alarmOriginatorName", alarmOriginatorName
"alarmOriginatorName", alarmOriginatorName,
"alarmOriginatorLabel", alarmOriginatorLabel
);
}

26
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmNotificationInfo.java

@ -25,11 +25,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ -41,25 +40,28 @@ public class AlarmNotificationInfo implements RuleOriginatedNotificationInfo {
private UUID alarmId;
private EntityId alarmOriginator;
private String alarmOriginatorName;
private String alarmOriginatorLabel;
private AlarmSeverity alarmSeverity;
private AlarmStatus alarmStatus;
private boolean acknowledged;
private boolean cleared;
private CustomerId alarmCustomerId;
private DashboardId dashboardId;
private Map<String, String> details;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"alarmType", alarmType,
"action", action,
"alarmId", alarmId.toString(),
"alarmSeverity", alarmSeverity.name().toLowerCase(),
"alarmStatus", alarmStatus.toString(),
"alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName(),
"alarmOriginatorName", alarmOriginatorName,
"alarmOriginatorId", alarmOriginator.getId().toString()
);
Map<String, String> templateData = details != null ? new HashMap<>(details) : new HashMap<>();
templateData.put("alarmType", alarmType);
templateData.put("action", action);
templateData.put("alarmId", alarmId.toString());
templateData.put("alarmSeverity", alarmSeverity.name().toLowerCase());
templateData.put("alarmStatus", alarmStatus.toString());
templateData.put("alarmOriginatorEntityType", alarmOriginator.getEntityType().getNormalName());
templateData.put("alarmOriginatorName", alarmOriginatorName);
templateData.put("alarmOriginatorLabel", alarmOriginatorLabel);
templateData.put("alarmOriginatorId", alarmOriginator.getId().toString());
return templateData;
}
@Override

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentTrigger.java

@ -25,10 +25,15 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
@Data
@Builder
public class AlarmCommentTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -8614770559491757202L;
private final TenantId tenantId;
private final AlarmComment comment;
private final Alarm alarm;

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmTrigger.java

@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
@Data
@Builder
public class AlarmTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -466810297904938644L;
private final TenantId tenantId;
private final AlarmApiCallResult alarmUpdate;

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmAssignmentNotificationRuleTriggerConfig.java

@ -23,6 +23,7 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import java.io.Serial;
import java.util.Set;
@Data
@ -31,6 +32,9 @@ import java.util.Set;
@Builder
public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
@Serial
private static final long serialVersionUID = -5313556049809972096L;
private Set<String> alarmTypes;
private Set<AlarmSeverity> alarmSeverities;
private Set<AlarmSearchStatus> alarmStatuses;

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmCommentNotificationRuleTriggerConfig.java

@ -22,6 +22,7 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import java.io.Serial;
import java.util.Set;
@Data
@ -30,6 +31,9 @@ import java.util.Set;
@Builder
public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
@Serial
private static final long serialVersionUID = -9164282098882339645L;
private Set<String> alarmTypes;
private Set<AlarmSeverity> alarmSeverities;
private Set<AlarmSearchStatus> alarmStatuses;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/AlarmNotificationRuleTriggerConfig.java

@ -23,6 +23,7 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import java.io.Serial;
import java.io.Serializable;
import java.util.Set;
@ -32,6 +33,9 @@ import java.util.Set;
@Builder
public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
@Serial
private static final long serialVersionUID = -7382883720381542344L;
private Set<String> alarmTypes;
private Set<AlarmSeverity> alarmSeverities;
@NotEmpty
@ -46,6 +50,8 @@ public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTrigg
@Data
public static class ClearRule implements Serializable {
@Serial
private static final long serialVersionUID = 7922533150038105124L;
private Set<AlarmSearchStatus> alarmStatuses;
}

13
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -41,9 +41,6 @@ import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Created by ashvayka on 13.01.18.
*/
@Data
@Slf4j
public final class TbMsg implements Serializable {
@ -500,11 +497,11 @@ public final class TbMsg implements Serializable {
public String toString() {
return "TbMsg.TbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts +
", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator +
", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType +
", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId +
", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds +
", ctx=" + this.ctx + ", callback=" + this.callback + ")";
", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator +
", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType +
", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId +
", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds +
", ctx=" + this.ctx + ", callback=" + this.callback + ")";
}
}

2
common/proto/src/main/proto/queue.proto

@ -62,7 +62,7 @@ enum EntityTypeProto {
MOBILE_APP = 37;
MOBILE_APP_BUNDLE = 38;
CALCULATED_FIELD = 39;
CALCULATED_FIELD_LINK = 40;
// CALCULATED_FIELD_LINK = 40; - was removed in 4.3
JOB = 41;
ADMIN_SETTINGS = 42;
AI_MODEL = 43;

4
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -151,12 +151,12 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
public Asset saveAsset(Asset asset, NameConflictStrategy nameConflictStrategy) {
return saveAsset(asset, true, nameConflictStrategy);
return saveEntity(asset, () -> saveAsset(asset, true, nameConflictStrategy));
}
@Override
public Asset saveAsset(Asset asset, boolean doValidate) {
return saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT);
return saveEntity(asset, () -> saveAsset(asset, doValidate, NameConflictStrategy.DEFAULT));
}
private Asset saveAsset(Asset asset, boolean doValidate, NameConflictStrategy nameConflictStrategy) {

62
dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java

@ -21,11 +21,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
@ -36,7 +34,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.service.validator.CalculatedFieldDataValidator;
import org.thingsboard.server.dao.service.validator.CalculatedFieldLinkDataValidator;
import java.util.EnumSet;
import java.util.List;
@ -57,9 +54,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
public static final String INCORRECT_ENTITY_ID = "Incorrect entityId ";
private final CalculatedFieldDao calculatedFieldDao;
private final CalculatedFieldLinkDao calculatedFieldLinkDao;
private final CalculatedFieldDataValidator calculatedFieldDataValidator;
private final CalculatedFieldLinkDataValidator calculatedFieldLinkDataValidator;
@Override
public CalculatedField save(CalculatedField calculatedField) {
@ -84,9 +79,13 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
log.trace("Executing save calculated field, [{}]", calculatedField);
updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis());
CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField);
createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId())
.entity(savedCalculatedField).oldEntity(oldCalculatedField).created(calculatedField.getId() == null).build());
eventPublisher.publishEvent(SaveEntityEvent.builder()
.tenantId(savedCalculatedField.getTenantId())
.entityId(savedCalculatedField.getId())
.entity(savedCalculatedField)
.oldEntity(oldCalculatedField)
.created(calculatedField.getId() == null)
.build());
return savedCalculatedField;
} catch (Exception e) {
checkConstraintViolation(e,
@ -191,48 +190,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return calculatedFields.size();
}
@Override
public CalculatedFieldLink saveCalculatedFieldLink(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) {
calculatedFieldLinkDataValidator.validate(calculatedFieldLink, CalculatedFieldLink::getTenantId);
log.trace("Executing save calculated field link, [{}]", calculatedFieldLink);
return calculatedFieldLinkDao.save(tenantId, calculatedFieldLink);
}
@Override
public CalculatedFieldLink findCalculatedFieldLinkById(TenantId tenantId, CalculatedFieldLinkId calculatedFieldLinkId) {
log.trace("Executing findCalculatedFieldLinkById, tenantId [{}], calculatedFieldLinkId [{}]", tenantId, calculatedFieldLinkId);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validateId(calculatedFieldLinkId, id -> "Incorrect calculatedFieldLinkId " + id);
return calculatedFieldLinkDao.findById(tenantId, calculatedFieldLinkId.getId());
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinksById(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
log.trace("Executing findAllCalculatedFieldLinksById, calculatedFieldId [{}]", calculatedFieldId);
return calculatedFieldLinkDao.findCalculatedFieldLinksByCalculatedFieldId(tenantId, calculatedFieldId);
}
@Override
public List<CalculatedFieldLink> findAllCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) {
log.trace("Executing findAllCalculatedFieldLinksByEntityId, entityId [{}]", entityId);
return calculatedFieldLinkDao.findCalculatedFieldLinksByEntityId(tenantId, entityId);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinksByTenantId(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinksByTenantId, tenantId[{}] pageLink [{}]", tenantId, pageLink);
validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
validatePageLink(pageLink);
return calculatedFieldLinkDao.findAllByTenantId(tenantId, pageLink);
}
@Override
public PageData<CalculatedFieldLink> findAllCalculatedFieldLinks(PageLink pageLink) {
log.trace("Executing findAllCalculatedFieldLinks, pageLink [{}]", pageLink);
validatePageLink(pageLink);
return calculatedFieldLinkDao.findAll(pageLink);
}
@Override
public boolean referencedInAnyCalculatedField(TenantId tenantId, EntityId referencedEntityId) {
return calculatedFieldDao.findAllByTenantId(tenantId).stream()
@ -258,9 +215,4 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
return EntityType.CALCULATED_FIELD;
}
private void createOrUpdateCalculatedFieldLink(TenantId tenantId, CalculatedField calculatedField) {
List<CalculatedFieldLink> links = calculatedField.getConfiguration().buildCalculatedFieldLinks(tenantId, calculatedField.getEntityId(), calculatedField.getId());
links.forEach(link -> saveCalculatedFieldLink(tenantId, link));
}
}

42
dao/src/main/java/org/thingsboard/server/dao/cf/CalculatedFieldLinkDao.java

@ -1,42 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.cf;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
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.Dao;
import java.util.List;
public interface CalculatedFieldLinkDao extends Dao<CalculatedFieldLink> {
List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId);
List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId);
List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId);
List<CalculatedFieldLink> findAll();
PageData<CalculatedFieldLink> findAll(PageLink pageLink);
PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink);
}

4
dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java

@ -143,13 +143,13 @@ public class CustomerServiceImpl extends AbstractCachedEntityService<CustomerCac
@Override
@Transactional
public Customer saveCustomer(Customer customer) {
return saveCustomer(customer, true, NameConflictStrategy.DEFAULT);
return saveCustomer(customer, NameConflictStrategy.DEFAULT);
}
@Override
@Transactional
public Customer saveCustomer(Customer customer, NameConflictStrategy nameConflictStrategy) {
return saveCustomer(customer, true, nameConflictStrategy);
return saveEntity(customer, () -> saveCustomer(customer, true, nameConflictStrategy));
}
private Customer saveCustomer(Customer customer, boolean doValidate) {

4
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -160,6 +160,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
@Override
public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) {
return saveEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate));
}
private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) {
log.trace("Executing saveDashboard [{}]", dashboard);
if (doValidate) {
dashboardValidator.validate(dashboard, DashboardInfo::getTenantId);

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -228,6 +228,10 @@ public class DeviceServiceImpl extends CachedVersionedEntityService<DeviceCacheK
}
private Device saveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
return saveEntity(device, () -> doSaveDeviceWithoutCredentials(device, doValidate, nameConflictStrategy));
}
private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate, NameConflictStrategy nameConflictStrategy) {
log.trace("Executing saveDevice [{}]", device);
Device oldDevice = (device.getId() != null) ? deviceDao.findById(device.getTenantId(), device.getId().getId()) : null;
if (nameConflictStrategy.policy() == NameConflictPolicy.UNIQUIFY && (oldDevice == null || !oldDevice.getName().equals(device.getName()))) {

4
dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java

@ -203,6 +203,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService<EdgeCacheKey, E
@Override
public Edge saveEdge(Edge edge) {
return saveEntity(edge, () -> doSaveEdge(edge));
}
private Edge doSaveEdge(Edge edge) {
log.trace("Executing saveEdge [{}]", edge);
Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId);
EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null);

21
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -21,11 +21,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.NameConflictStrategy;
@ -51,8 +53,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -64,6 +69,8 @@ public abstract class AbstractEntityService {
public static final String INCORRECT_EDGE_ID = "Incorrect edgeId ";
public static final String INCORRECT_PAGE_LINK = "Incorrect page link ";
private final ConcurrentMap<TenantId, ReentrantLock> entityCreationLocks = new ConcurrentReferenceHashMap<>(16);
@Autowired
protected ApplicationEventPublisher eventPublisher;
@ -101,6 +108,20 @@ public abstract class AbstractEntityService {
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
protected <E extends HasId & HasTenantId> E saveEntity(E entity, Supplier<E> saveFunction) {
if (entity.getId() == null) {
ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock());
lock.lock();
try {
return saveFunction.get();
} finally {
lock.unlock();
}
} else {
return saveFunction.get();
}
}
protected void createRelation(TenantId tenantId, EntityRelation relation) {
log.debug("Creating relation: {}", relation);
relationService.saveRelation(tenantId, relation);

3
dao/src/main/java/org/thingsboard/server/dao/entity/DefaultEntityServiceRegistry.java

@ -43,9 +43,6 @@ public class DefaultEntityServiceRegistry implements EntityServiceRegistry {
if (EntityType.RULE_CHAIN.equals(entityType)) {
entityDaoServicesMap.put(EntityType.RULE_NODE, entityDaoService);
}
if (EntityType.CALCULATED_FIELD.equals(entityType)) {
entityDaoServicesMap.put(EntityType.CALCULATED_FIELD_LINK, entityDaoService);
}
});
log.debug("Initialized EntityServiceRegistry total [{}] entries", entityDaoServicesMap.size());
}

12
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -26,8 +26,7 @@ import java.util.UUID;
public class ModelConstants {
private ModelConstants() {
}
private ModelConstants() {}
public static final UUID NULL_UUID = Uuids.startOf(0);
public static final TenantId SYSTEM_TENANT = TenantId.fromUUID(ModelConstants.NULL_UUID);
@ -731,15 +730,6 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version";
/**
* Calculated field links constants.
*/
public static final String CALCULATED_FIELD_LINK_TABLE_NAME = "calculated_field_link";
public static final String CALCULATED_FIELD_LINK_TENANT_ID_COLUMN = TENANT_ID_COLUMN;
public static final String CALCULATED_FIELD_LINK_ENTITY_TYPE = ENTITY_TYPE_COLUMN;
public static final String CALCULATED_FIELD_LINK_ENTITY_ID = ENTITY_ID_COLUMN;
public static final String CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID = "calculated_field_id";
/**
* Tasks constants.
*/

79
dao/src/main/java/org/thingsboard/server/dao/model/sql/CalculatedFieldLinkEntity.java

@ -1,79 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.model.sql;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_ENTITY_TYPE;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_LINK_TENANT_ID_COLUMN;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = CALCULATED_FIELD_LINK_TABLE_NAME)
public class CalculatedFieldLinkEntity extends BaseSqlEntity<CalculatedFieldLink> implements BaseEntity<CalculatedFieldLink> {
@Column(name = CALCULATED_FIELD_LINK_TENANT_ID_COLUMN)
private UUID tenantId;
@Column(name = CALCULATED_FIELD_LINK_ENTITY_TYPE)
private String entityType;
@Column(name = CALCULATED_FIELD_LINK_ENTITY_ID)
private UUID entityId;
@Column(name = CALCULATED_FIELD_LINK_CALCULATED_FIELD_ID)
private UUID calculatedFieldId;
public CalculatedFieldLinkEntity() {
super();
}
public CalculatedFieldLinkEntity(CalculatedFieldLink calculatedFieldLink) {
super(calculatedFieldLink);
this.tenantId = calculatedFieldLink.getTenantId().getId();
this.entityType = calculatedFieldLink.getEntityId().getEntityType().name();
this.entityId = calculatedFieldLink.getEntityId().getId();
this.calculatedFieldId = calculatedFieldLink.getCalculatedFieldId().getId();
}
@Override
public CalculatedFieldLink toData() {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(new CalculatedFieldLinkId(id));
calculatedFieldLink.setCreatedTime(createdTime);
calculatedFieldLink.setTenantId(TenantId.fromUUID(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
return calculatedFieldLink;
}
}

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

@ -127,6 +127,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
@Override
@Transactional
public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) {
return saveEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, doValidate));
}
private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) {
log.trace("Executing doSaveRuleChain [{}]", ruleChain);
if (doValidate) {
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);

41
dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidator.java

@ -1,41 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@Component
public class CalculatedFieldLinkDataValidator extends DataValidator<CalculatedFieldLink> {
@Autowired
private CalculatedFieldLinkDao calculatedFieldLinkDao;
@Override
protected CalculatedFieldLink validateUpdate(TenantId tenantId, CalculatedFieldLink calculatedFieldLink) {
CalculatedFieldLink old = calculatedFieldLinkDao.findById(calculatedFieldLink.getTenantId(), calculatedFieldLink.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing calculated field link!");
}
return old;
}
}

36
dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldLinkRepository.java

@ -1,36 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
import java.util.List;
import java.util.UUID;
public interface CalculatedFieldLinkRepository extends JpaRepository<CalculatedFieldLinkEntity, UUID> {
List<CalculatedFieldLinkEntity> findAllByTenantIdAndCalculatedFieldId(UUID tenantId, UUID calculatedFieldId);
List<CalculatedFieldLinkEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
List<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId);
Page<CalculatedFieldLinkEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
}

38
dao/src/main/java/org/thingsboard/server/dao/sql/cf/DefaultNativeCalculatedFieldRepository.java

@ -25,12 +25,10 @@ import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -49,9 +47,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
private final String CF_COUNT_QUERY = "SELECT count(id) FROM calculated_field;";
private final String CF_QUERY = "SELECT * FROM calculated_field ORDER BY created_time ASC LIMIT %s OFFSET %s";
private final String CFL_COUNT_QUERY = "SELECT count(id) FROM calculated_field_link;";
private final String CFL_QUERY = "SELECT * FROM calculated_field_link ORDER BY created_time ASC LIMIT %s OFFSET %s";
private final NamedParameterJdbcTemplate jdbcTemplate;
private final TransactionTemplate transactionTemplate;
@ -103,37 +98,4 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
});
}
@Override
public PageData<CalculatedFieldLink> findCalculatedFieldLinks(Pageable pageable) {
return transactionTemplate.execute(status -> {
long startTs = System.currentTimeMillis();
int totalElements = jdbcTemplate.queryForObject(CFL_COUNT_QUERY, Collections.emptyMap(), Integer.class);
log.debug("Count query took {} ms", System.currentTimeMillis() - startTs);
startTs = System.currentTimeMillis();
List<Map<String, Object>> rows = jdbcTemplate.queryForList(String.format(CFL_QUERY, pageable.getPageSize(), pageable.getOffset()), Collections.emptyMap());
log.debug("Main query took {} ms", System.currentTimeMillis() - startTs);
int totalPages = pageable.getPageSize() > 0 ? (int) Math.ceil((float) totalElements / pageable.getPageSize()) : 1;
boolean hasNext = pageable.getPageSize() > 0 && totalElements > pageable.getOffset() + rows.size();
var data = rows.stream().map(row -> {
UUID id = (UUID) row.get("id");
long createdTime = (long) row.get("created_time");
UUID tenantId = (UUID) row.get("tenant_id");
EntityType entityType = EntityType.valueOf((String) row.get("entity_type"));
UUID entityId = (UUID) row.get("entity_id");
UUID calculatedFieldId = (UUID) row.get("calculated_field_id");
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setId(new CalculatedFieldLinkId(id));
calculatedFieldLink.setCreatedTime(createdTime);
calculatedFieldLink.setTenantId(new TenantId(tenantId));
calculatedFieldLink.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(calculatedFieldId));
return calculatedFieldLink;
}).collect(Collectors.toList());
return new PageData<>(data, totalPages, totalElements, hasNext);
});
}
}

94
dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldLinkDao.java

@ -1,94 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.cf;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
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.DaoUtil;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.model.sql.CalculatedFieldLinkEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
import java.util.UUID;
@Slf4j
@Component
@AllArgsConstructor
@SqlDao
public class JpaCalculatedFieldLinkDao extends JpaAbstractDao<CalculatedFieldLinkEntity, CalculatedFieldLink> implements CalculatedFieldLinkDao {
private final CalculatedFieldLinkRepository calculatedFieldLinkRepository;
private final NativeCalculatedFieldRepository nativeCalculatedFieldRepository;
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByCalculatedFieldId(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndCalculatedFieldId(tenantId.getId(), calculatedFieldId.getId()));
}
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByEntityId(TenantId tenantId, EntityId entityId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId()));
}
@Override
public List<CalculatedFieldLink> findCalculatedFieldLinksByTenantId(TenantId tenantId) {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId()));
}
@Override
public List<CalculatedFieldLink> findAll() {
return DaoUtil.convertDataList(calculatedFieldLinkRepository.findAll());
}
@Override
public PageData<CalculatedFieldLink> findAll(PageLink pageLink) {
log.debug("Try to find calculated field links by pageLink [{}]", pageLink);
return nativeCalculatedFieldRepository.findCalculatedFieldLinks(DaoUtil.toPageable(pageLink));
}
@Override
public PageData<CalculatedFieldLink> findAllByTenantId(TenantId tenantId, PageLink pageLink) {
log.debug("Try to find calculated field links by tenantId [{}], pageLink [{}]", tenantId, pageLink);
return DaoUtil.toPageData(calculatedFieldLinkRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)));
}
@Override
protected Class<CalculatedFieldLinkEntity> getEntityClass() {
return CalculatedFieldLinkEntity.class;
}
@Override
protected JpaRepository<CalculatedFieldLinkEntity, UUID> getRepository() {
return calculatedFieldLinkRepository;
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD_LINK;
}
}

3
dao/src/main/java/org/thingsboard/server/dao/sql/cf/NativeCalculatedFieldRepository.java

@ -17,13 +17,10 @@ package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.domain.Pageable;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.page.PageData;
public interface NativeCalculatedFieldRepository {
PageData<CalculatedField> findCalculatedFields(Pageable pageable);
PageData<CalculatedFieldLink> findCalculatedFieldLinks(Pageable pageable);
}

4
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -170,6 +170,10 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
@Override
@Transactional
public User saveUser(TenantId tenantId, User user) {
return saveEntity(user, () -> doSaveUser(tenantId, user));
}
private User doSaveUser(TenantId tenantId, User user) {
log.trace("Executing saveUser [{}]", user);
User oldUser = userValidator.validate(user, User::getTenantId);
if (!userLoginCaseSensitive) {

10
dao/src/main/resources/sql/schema-entities.sql

@ -926,16 +926,6 @@ CREATE TABLE IF NOT EXISTS calculated_field (
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name)
);
CREATE TABLE IF NOT EXISTS calculated_field_link (
id uuid NOT NULL CONSTRAINT calculated_field_link_pkey PRIMARY KEY,
created_time bigint NOT NULL,
tenant_id uuid NOT NULL,
entity_type VARCHAR(32),
entity_id uuid NOT NULL,
calculated_field_id uuid NOT NULL,
CONSTRAINT fk_calculated_field_id FOREIGN KEY (calculated_field_id) REFERENCES calculated_field(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cf_debug_event (
id uuid NOT NULL,
tenant_id uuid NOT NULL ,

6
dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java

@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
@ -185,8 +186,13 @@ public abstract class AbstractServiceTest {
}
public Tenant createTenant() {
return createTenant(null);
}
public Tenant createTenant(TenantProfileId tenantProfileId) {
Tenant tenant = new Tenant();
tenant.setTitle("My tenant " + UUID.randomUUID());
tenant.setTenantProfileId(tenantProfileId);
Tenant savedTenant = tenantService.saveTenant(tenant);
assertNotNull(savedTenant);
return savedTenant;

62
dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java

@ -16,17 +16,25 @@
package org.thingsboard.server.dao.service;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -43,6 +51,8 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.asset.AssetDao;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
@ -50,12 +60,16 @@ import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ -71,13 +85,28 @@ public class AssetServiceTest extends AbstractServiceTest {
@Autowired
RelationService relationService;
@Autowired
TenantProfileService tenantProfileService;
@Autowired
private AssetProfileService assetProfileService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private PlatformTransactionManager platformTransactionManager;
private static ListeningExecutorService executor;
private IdComparator<Asset> idComparator = new IdComparator<>();
private TenantId anotherTenantId;
@BeforeClass
public static void before() {
executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("AssetServiceTestScope")));
}
@AfterClass
public static void after() {
executor.shutdownNow();
}
@Test
public void testSaveAsset() {
@ -104,6 +133,39 @@ public class AssetServiceTest extends AbstractServiceTest {
assetService.deleteAsset(tenantId, savedAsset.getId());
}
@Test
public void testAssetLimitOnTenantProfileLevel() throws InterruptedException {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName("Test profile");
tenantProfile.setDescription("Test");
TenantProfileData profileData = new TenantProfileData();
profileData.setConfiguration(DefaultTenantProfileConfiguration.builder().maxAssets(5l).build());
tenantProfile.setProfileData(profileData);
tenantProfile.setDefault(false);
tenantProfile.setIsolatedTbRuleEngine(false);
tenantProfile = tenantProfileService.saveTenantProfile(anotherTenantId, tenantProfile);
anotherTenantId = createTenant(tenantProfile.getId()).getId();
for (int i = 0; i < 20; i++) {
executor.submit(() -> {
Asset asset = new Asset();
asset.setTenantId(anotherTenantId);
asset.setName(RandomStringUtils.randomAlphabetic(10));
asset.setType("default");
assetService.saveAsset(asset);
});
}
Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> {
long countByTenantId = assetService.countByTenantId(anotherTenantId);
return countByTenantId == 5;
});
Thread.sleep(2000);
assertThat(assetService.countByTenantId(anotherTenantId)).isEqualTo(5);
}
@Test
public void testShouldNotPutInCacheRolledbackAssetProfile() {
AssetProfile assetProfile = new AssetProfile();

19
dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java

@ -15,13 +15,8 @@
*/
package org.thingsboard.server.dao.service;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -63,18 +58,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
@Autowired
private TbTenantProfileCache tbTenantProfileCache;
private ListeningExecutorService executor;
@Before
public void before() {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
}
@After
public void after() {
executor.shutdownNow();
}
@Test
public void testSaveCalculatedField() {
Device device = createTestDevice();
@ -143,7 +126,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
.isInstanceOf(DataValidationException.class)
.hasCauseInstanceOf(IllegalArgumentException.class)
.hasMessageStartingWith("Scheduled update interval is less than configured " +
"minimum allowed interval in tenant profile: ");
"minimum allowed interval in tenant profile: ");
}
@Test

45
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -16,9 +16,13 @@
package org.thingsboard.server.dao.service;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.Mockito;
@ -27,6 +31,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.testcontainers.shaded.org.awaitility.Awaitility;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
@ -73,7 +79,10 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
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.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
@ -104,6 +113,17 @@ public class DeviceServiceTest extends AbstractServiceTest {
private IdComparator<Device> idComparator = new IdComparator<>();
private TenantId anotherTenantId;
private static ListeningExecutorService executor;
@BeforeClass
public static void beforeClass() {
executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("DeviceServiceTestScope")));
}
@AfterClass
public static void afterClass() {
executor.shutdownNow();
}
@Before
public void before() {
@ -135,6 +155,31 @@ public class DeviceServiceTest extends AbstractServiceTest {
deleteDevice(tenantId, device);
}
@Test
public void testDeviceLimitOnTenantProfileLevel() throws InterruptedException {
TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId);
defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build());
tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile);
for (int i = 0; i < 20; i++) {
executor.submit(() -> {
Device device = new Device();
device.setTenantId(tenantId);
device.setName(StringUtils.randomAlphabetic(10));
device.setType("default");
deviceService.saveDevice(device, true);
});
}
Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> {
long countByTenantId = deviceService.countByTenantId(tenantId);
return countByTenantId == 5;
});
Thread.sleep(2000);
assertThat(deviceService.countByTenantId(tenantId)).isEqualTo(5);
}
@Test
public void testSaveDevicesWithMaxDeviceOutOfLimit() {
TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId);

5
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceRegistryTest.java

@ -20,7 +20,6 @@ import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.entity.EntityDaoService;
import org.thingsboard.server.dao.entity.EntityServiceRegistry;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -45,8 +44,4 @@ public class EntityServiceRegistryTest extends AbstractServiceTest {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.RULE_NODE) instanceof RuleChainService);
}
@Test
public void givenCalculatedFieldLinkEntityType_whenGetServiceByEntityTypeCalled_thenReturnedCalculatedFieldService() {
Assert.assertTrue(entityServiceRegistry.getServiceByEntityType(EntityType.CALCULATED_FIELD_LINK) instanceof CalculatedFieldService);
}
}

10
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java

@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -38,11 +38,11 @@ public class CalculatedFieldDataValidatorTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("7b5229e9-166e-41a9-a257-3b1dafad1b04"));
private final CalculatedFieldId CALCULATED_FIELD_ID = new CalculatedFieldId(UUID.fromString("060fbe45-fbb2-4549-abf3-f72a6be3cb9f"));
@MockBean
@MockitoBean
private CalculatedFieldDao calculatedFieldDao;
@MockBean
@MockitoBean
private DefaultApiLimitService apiLimitService;
@SpyBean
@MockitoSpyBean
private CalculatedFieldDataValidator validator;
@Test

57
dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldLinkDataValidatorTest.java

@ -1,57 +0,0 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.cf.CalculatedFieldLinkDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
@SpringBootTest(classes = CalculatedFieldLinkDataValidator.class)
public class CalculatedFieldLinkDataValidatorTest {
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("2ba09d99-6143-43dc-b645-381fc0c43ebe"));
private final CalculatedFieldLinkId CALCULATED_FIELD_LINK_ID = new CalculatedFieldLinkId(UUID.fromString("a5609ef4-cb42-43ce-9b23-e090a4878d1c"));
@MockBean
private CalculatedFieldLinkDao calculatedFieldLinkDao;
@SpyBean
private CalculatedFieldLinkDataValidator validator;
@Test
public void testUpdateNonExistingCalculatedField() {
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink(CALCULATED_FIELD_LINK_ID);
calculatedFieldLink.setCalculatedFieldId(new CalculatedFieldId(UUID.fromString("136477af-fd07-4498-b9c9-54fe50e82992")));
given(calculatedFieldLinkDao.findById(TENANT_ID, CALCULATED_FIELD_LINK_ID.getId())).willReturn(null);
assertThatThrownBy(() -> validator.validateUpdate(TENANT_ID, calculatedFieldLink))
.isInstanceOf(DataValidationException.class)
.hasMessage("Can't update non existing calculated field link!");
}
}

10
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

@ -17,7 +17,7 @@ package org.thingsboard.server.msa.cf;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@ -600,7 +600,7 @@ public class CalculatedFieldTest extends AbstractContainerTest {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5));
calculatedField.setName("C to F" + RandomStringUtils.insecure().nextAlphabetic(5));
calculatedField.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
@ -624,15 +624,11 @@ public class CalculatedFieldTest extends AbstractContainerTest {
return testRestClient.postCalculatedField(calculatedField);
}
private CalculatedField createScriptCalculatedField() {
return createScriptCalculatedField(device.getId(), asset.getId());
}
private CalculatedField createScriptCalculatedField(EntityId entityId, EntityId refEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SCRIPT);
calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5));
calculatedField.setName("Air density" + RandomStringUtils.insecure().nextAlphabetic(5));
calculatedField.setDebugSettings(DebugSettings.all());
ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration();

128
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/JavaRestClientTest.java

@ -0,0 +1,128 @@
/**
* 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.msa.connectivity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.HostnameVerificationPolicy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.ssl.SSLContexts;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.TestProperties;
import javax.net.ssl.SSLContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
public class JavaRestClientTest extends AbstractContainerTest {
private RestClient restClient;
@BeforeClass
public void beforeClass() throws Exception {
SSLContext ssl = SSLContexts.custom()
.loadTrustMaterial((chain, authType) -> true)
.build();
var tls = new DefaultClientTlsStrategy(
ssl,
HostnameVerificationPolicy.CLIENT,
NoopHostnameVerifier.INSTANCE
);
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tls)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
RestTemplate rt = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
restClient = new RestClient(rt, TestProperties.getBaseUrl());
}
@BeforeMethod
public void setUp() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
}
@AfterMethod
public void tearDown() {
}
@Test
public void testGetAlarmsV2() {
Device device = restClient.saveDevice(defaultDevicePrototype(RandomStringUtils.randomAlphabetic(5)));
assertThat(device).isNotNull();
String type = "High temp" + RandomStringUtils.randomAlphabetic(5);
Alarm alarm = Alarm.builder()
.originator(device.getId())
.severity(AlarmSeverity.CRITICAL)
.type(type)
.build();
restClient.saveAlarm(alarm);
// get /api/v2/alarm
PageData<AlarmInfo> alarmsV2 = restClient.getAlarmsV2(device.getId(), null, null, List.of(type), null, new TimePageLink(10, 0));
assertThat(alarmsV2.getData()).hasSize(1);
PageData<AlarmInfo> activeAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(activeAlarms.getData()).hasSize(1);
PageData<AlarmInfo> cleared = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(cleared.getData()).hasSize(0);
PageData<AlarmInfo> activeAndClearedAlarms = restClient.getAlarmsV2(device.getId(), List.of(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACTIVE), null, null, null, new TimePageLink(10, 0));
assertThat(activeAndClearedAlarms.getData()).hasSize(1);
// get /api/v2/alarms
PageData<AlarmInfo> allAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.ACTIVE), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(allAlarmsV2.getData()).hasSize(1);
PageData<AlarmInfo> allClearedAlarmsV2 = restClient.getAllAlarmsV2(List.of(AlarmSearchStatus.CLEARED), null, List.of(type), null, new TimePageLink(10, 0));
assertThat(allClearedAlarmsV2.getData()).hasSize(0);
// get /api/alarms
PageData<AlarmInfo> allAlarms = restClient.getAllAlarms(AlarmSearchStatus.ACTIVE, null, new TimePageLink(10, 0), null);
assertThat(allAlarms.getData()).hasSize(1);
PageData<AlarmInfo> allClearedAlarms = restClient.getAllAlarms(AlarmSearchStatus.CLEARED, null, new TimePageLink(10, 0), null);
assertThat(allClearedAlarms.getData()).hasSize(0);
}
}

271
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -115,6 +115,8 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.MobileAppBundleId;
import org.thingsboard.server.common.data.id.MobileAppId;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.OAuth2ClientId;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.id.OtaPackageId;
@ -134,6 +136,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.mobile.app.MobileApp;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundle;
import org.thingsboard.server.common.data.mobile.bundle.MobileAppBundleInfo;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.oauth2.OAuth2Client;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientLoginInfo;
@ -511,6 +520,99 @@ public class RestClient implements Closeable {
params).getBody();
}
public PageData<AlarmInfo> getAllAlarms(AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) {
String urlSecondPart = "/api/alarms?";
Map<String, String> params = new HashMap<>();
if (fetchOriginator != null) {
params.put("fetchOriginator", String.valueOf(fetchOriginator));
urlSecondPart += "&fetchOriginator={fetchOriginator}";
}
if (searchStatus != null) {
params.put("searchStatus", searchStatus.name());
urlSecondPart += "&searchStatus={searchStatus}";
}
if (status != null) {
params.put("status", status.name());
urlSecondPart += "&status={status}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public PageData<AlarmInfo> getAlarmsV2(EntityId entityId, List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarm/{entityType}/{entityId}?";
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
params.put("entityId", entityId.getId().toString());
if (!CollectionUtils.isEmpty(statusList)) {
params.put("statusList", listEnumToString(statusList));
urlSecondPart += "&statusList={statusList}";
}
if (!CollectionUtils.isEmpty(severityList)) {
params.put("severityList", listEnumToString(severityList));
urlSecondPart += "&severityList={severityList}";
}
if (!CollectionUtils.isEmpty(typeList)) {
params.put("typeList", String.join(",", typeList));
urlSecondPart += "&typeList={typeList}";
}
if (assignedId != null) {
params.put("assignedId", assignedId);
urlSecondPart += "&assignedId={assignedId}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public PageData<AlarmInfo> getAllAlarmsV2(List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarms?";
Map<String, String> params = new HashMap<>();
if (!CollectionUtils.isEmpty(statusList)) {
params.put("statusList", listEnumToString(statusList));
urlSecondPart += "&statusList={statusList}";
}
if (!CollectionUtils.isEmpty(severityList)) {
params.put("severityList", listEnumToString(severityList));
urlSecondPart += "&severityList={severityList}";
}
if (!CollectionUtils.isEmpty(typeList)) {
params.put("typeList", String.join(",", typeList));
urlSecondPart += "&typeList={typeList}";
}
if (assignedId != null) {
params.put("assignedId", assignedId);
urlSecondPart += "&assignedId={assignedId}";
}
addTimePageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + urlSecondPart + "&" + getTimeUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<AlarmInfo>>() {
},
params).getBody();
}
public Optional<AlarmSeverity> getHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status) {
Map<String, String> params = new HashMap<>();
params.put("entityType", entityId.getEntityType().name());
@ -1712,6 +1814,14 @@ public class RestClient implements Closeable {
}).getBody();
}
public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/entitiesQuery/find/keys",
HttpMethod.POST, new HttpEntity<>(query),
new ParameterizedTypeReference<JsonNode>() {
}).getBody();
}
public PageData<AlarmData> findAlarmDataByQuery(AlarmDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/alarmsQuery/find",
@ -2160,9 +2270,11 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/oauth2/client/{id}", oAuth2ClientId.getId());
}
public PageData<DomainInfo> getTenantDomainInfos() {
public PageData<DomainInfo> getTenantDomainInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/domain/infos",
baseURL + "/api/domain/infos?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<DomainInfo>>() {
@ -2194,9 +2306,11 @@ public class RestClient implements Closeable {
restTemplate.postForLocation(baseURL + "/api/domain/{id}/oauth2Clients", oauth2ClientIds, domainId.getId());
}
public PageData<MobileApp> getTenantMobileApps() {
public PageData<MobileApp> getTenantMobileApps(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/mobile/app",
baseURL + "/api/mobile/app?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileApp>>() {
@ -2224,9 +2338,11 @@ public class RestClient implements Closeable {
restTemplate.delete(baseURL + "/api/mobile/app/{id}", mobileAppId.getId());
}
public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos() {
public PageData<MobileAppBundleInfo> getTenantMobileBundleInfos(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/mobile/bundle/infos",
baseURL + "/api/mobile/bundle/infos?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<MobileAppBundleInfo>>() {
@ -2848,6 +2964,17 @@ public class RestClient implements Closeable {
}, params).getBody();
}
public PageData<UserEmailInfo> getUsersByQuery(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/users/info?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<UserEmailInfo>>() {
}, params).getBody();
}
public PageData<User> getTenantAdmins(TenantId tenantId, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("tenantId", tenantId.getId().toString());
@ -4146,6 +4273,138 @@ public class RestClient implements Closeable {
}
}
public PageData<Notification> getNotifications(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/notifications?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<Notification>>() {
}, params).getBody();
}
public Integer getUnreadNotificationsCount(NotificationDeliveryMethod deliveryMethod) {
String uri = "/api/notifications/unread/count?";
Map<String, String> params = new HashMap<>();
if (deliveryMethod != null) {
params.put("deliveryMethod", deliveryMethod.name());
uri += "&deliveryMethod={deliveryMethod}";
}
return restTemplate.exchange(
baseURL + uri,
HttpMethod.GET,
HttpEntity.EMPTY,
Integer.class, params).getBody();
}
public void markNotificationAsRead(NotificationId notificationId) {
restTemplate.exchange(
baseURL + "/api/notification/{id}/read",
HttpMethod.PUT,
HttpEntity.EMPTY,
Void.class,
notificationId.getId());
}
public void markAllNotificationsAsRead(NotificationDeliveryMethod deliveryMethod) {
String uri = "/api/notifications/read?";
Map<String, String> params = new HashMap<>();
if (deliveryMethod != null) {
params.put("deliveryMethod", deliveryMethod.name());
uri += "&deliveryMethod={deliveryMethod}";
}
restTemplate.exchange(
baseURL + uri,
HttpMethod.PUT,
HttpEntity.EMPTY,
Void.class);
}
public void deleteNotification(NotificationId notificationId) {
restTemplate.delete(baseURL + "/api/notification/{id}", notificationId.getId());
}
public NotificationRequest createNotificationRequest(NotificationRequest notificationRequest) {
return restTemplate.postForEntity(baseURL + "/api/notification/request", notificationRequest, NotificationRequest.class).getBody();
}
public NotificationRequestPreview getNotificationRequestPreview(NotificationRequest notificationRequest, int recipientsPreviewSize) {
return restTemplate.postForEntity(baseURL + "/api/notification/request/preview?recipientsPreviewSize={recipientsPreviewSize}", notificationRequest, NotificationRequestPreview.class, recipientsPreviewSize).getBody();
}
public Optional<NotificationRequestInfo> getNotificationRequestById(NotificationRequestId notificationRequestId) {
try {
ResponseEntity<NotificationRequestInfo> notificationRequest = restTemplate.getForEntity(baseURL + "/api/notification/request/{id}", NotificationRequestInfo.class, notificationRequestId.getId());
return Optional.ofNullable(notificationRequest.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public PageData<NotificationRequestInfo> getNotificationRequests(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/notification/requests?" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<NotificationRequestInfo>>() {
}, params).getBody();
}
public void deleteNotificationRequest(NotificationRequestId notificationRequestId) {
restTemplate.delete(baseURL + "/api/notification/request/{id}", notificationRequestId.getId());
}
public NotificationSettings saveNotificationSettings(NotificationSettings notificationSettings) {
return restTemplate.postForEntity(baseURL + "/api/notification/settings", notificationSettings, NotificationSettings.class).getBody();
}
public Optional<NotificationSettings> getNotificationSettings() {
try {
ResponseEntity<NotificationSettings> notificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings", NotificationSettings.class);
return Optional.ofNullable(notificationSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public List<NotificationDeliveryMethod> getAvailableDeliveryMethods() {
return restTemplate.exchange(URI.create(
baseURL + "/api/notification/deliveryMethods"),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<NotificationDeliveryMethod>>() {
}).getBody();
}
public UserNotificationSettings saveUserNotificationSettings(UserNotificationSettings userNotificationSettings) {
return restTemplate.postForEntity(baseURL + "/api/notification/settings/user", userNotificationSettings, UserNotificationSettings.class).getBody();
}
public Optional<UserNotificationSettings> getUserNotificationSettings() {
try {
ResponseEntity<UserNotificationSettings> userNotificationSettings = restTemplate.getForEntity(baseURL + "/api/notification/settings/user", UserNotificationSettings.class);
return Optional.ofNullable(userNotificationSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public AiModel saveAiModel(AiModel aiModel) {
return restTemplate.postForEntity(baseURL + "/api/ai/model", aiModel, AiModel.class).getBody();
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java

@ -18,14 +18,12 @@ package org.thingsboard.rule.engine.util;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AiModelId;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CalculatedFieldLinkId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -170,14 +168,6 @@ public class TenantIdLoader {
case CALCULATED_FIELD:
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, new CalculatedFieldId(id));
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = ctx.getCalculatedFieldService().findCalculatedFieldLinkById(ctxTenantId, new CalculatedFieldLinkId(id));
if (calculatedFieldLink != null) {
tenantEntity = ctx.getCalculatedFieldService().findById(ctxTenantId, calculatedFieldLink.getCalculatedFieldId());
} else {
tenantEntity = null;
}
break;
case JOB:
tenantEntity = ctx.getJobService().findJobById(ctxTenantId, new JobId(id));
break;

11
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/TenantIdLoaderTest.java

@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -46,14 +45,12 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.job.Job;
@ -80,6 +77,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
import org.thingsboard.server.dao.mobile.MobileAppService;
import org.thingsboard.server.dao.notification.NotificationRequestService;
@ -92,7 +90,6 @@ import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@ -422,12 +419,6 @@ public class TenantIdLoaderTest {
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedField).when(calculatedFieldService).findById(eq(tenantId), any());
break;
case CALCULATED_FIELD_LINK:
CalculatedFieldLink calculatedFieldLink = new CalculatedFieldLink();
calculatedFieldLink.setTenantId(tenantId);
when(ctx.getCalculatedFieldService()).thenReturn(calculatedFieldService);
doReturn(calculatedFieldLink).when(calculatedFieldService).findCalculatedFieldLinkById(eq(tenantId), any());
break;
case JOB:
Job job = new Job();
job.setTenantId(tenantId);

81
ui-ngx/src/app/core/http/attribute.service.ts

@ -15,7 +15,7 @@
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { EntityId } from '@shared/models/id/entity-id';
@ -35,47 +35,37 @@ export class AttributeService {
public getEntityAttributes(entityId: EntityId, attributeScope?: AttributeScope,
keys?: Array<string>, config?: RequestConfig): Observable<Array<AttributeData>> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/attributes`;
let queryParams: object = null;
if (attributeScope) {
url += `/${attributeScope}`;
}
if (keys && keys.length) {
url += `?keys=${keys.join(',')}`;
queryParams = {key: keys};
}
return this.http.get<Array<AttributeData>>(url, defaultHttpOptionsFromConfig(config));
return this.http.get<Array<AttributeData>>(url, defaultHttpOptionsFromParams(queryParams, config));
}
public deleteEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
config?: RequestConfig): Observable<any> {
const keys = attributes.map(attribute => encodeURIComponent(attribute.key)).join(',');
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}` +
`?keys=${keys}`,
defaultHttpOptionsFromConfig(config));
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/${attributeScope}`,
defaultHttpOptionsFromParams({key: attributes.map(attribute => attribute.key)}, config));
}
public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false,
startTs?: number, endTs?: number, rewriteLatestIfDeleted = false, deleteLatest = true,
config?: RequestConfig): Observable<any> {
const keys = timeseries.map(attribute => encodeURIComponent(attribute.key)).join(',');
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete?keys=${keys}`;
if (isDefinedAndNotNull(deleteAllDataForKeys)) {
url += `&deleteAllDataForKeys=${deleteAllDataForKeys}`;
}
if (isDefinedAndNotNull(rewriteLatestIfDeleted)) {
url += `&rewriteLatestIfDeleted=${rewriteLatestIfDeleted}`;
}
if (isDefinedAndNotNull(deleteLatest)) {
url += `&deleteLatest=${deleteLatest}`;
}
if (isDefinedAndNotNull(startTs)) {
url += `&startTs=${startTs}`;
}
if (isDefinedAndNotNull(endTs)) {
url += `&endTs=${endTs}`;
}
return this.http.delete(url, defaultHttpOptionsFromConfig(config));
const queryParams = {
key: timeseries.map(key => key.key),
deleteAllDataForKeys: deleteAllDataForKeys,
rewriteLatestIfDeleted: rewriteLatestIfDeleted,
deleteLatest: deleteLatest,
startTs: startTs,
endTs: endTs
};
return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete`,
defaultHttpOptionsFromParams(queryParams, config));
}
public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>,
@ -138,32 +128,29 @@ export class AttributeService {
limit: number = 100, agg: AggregationType = AggregationType.NONE, interval?: number,
orderBy: DataSortOrder = DataSortOrder.DESC, useStrictDataTypes: boolean = false,
config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?keys=${keys.join(',')}&startTs=${startTs}&endTs=${endTs}`;
if (isDefinedAndNotNull(limit)) {
url += `&limit=${limit}`;
}
if (isDefinedAndNotNull(agg)) {
url += `&agg=${agg}`;
}
if (isDefinedAndNotNull(interval)) {
url += `&interval=${interval}`;
}
if (isDefinedAndNotNull(orderBy)) {
url += `&orderBy=${orderBy}`;
}
if (isDefinedAndNotNull(useStrictDataTypes)) {
url += `&useStrictDataTypes=${useStrictDataTypes}`;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
const queryParams = {
key: keys,
startTs: startTs,
endTs: endTs,
limit: limit,
agg: agg,
interval: interval,
orderBy: orderBy,
useStrictDataTypes: useStrictDataTypes
}
return this.http.get<TimeseriesData>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`,
defaultHttpOptionsFromParams(queryParams, config));
}
public getEntityTimeseriesLatest(entityId: EntityId, keys?: Array<string>,
useStrictDataTypes = false, config?: RequestConfig): Observable<TimeseriesData> {
let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries?useStrictDataTypes=${useStrictDataTypes}`;
const queryParams: Record<string, any> = {
useStrictDataTypes: useStrictDataTypes
}
if (isDefinedAndNotNull(keys) && keys.length) {
url += `&keys=${keys.join(',')}`;
queryParams.key = keys;
}
return this.http.get<TimeseriesData>(url, defaultHttpOptionsFromConfig(config));
return this.http.get<TimeseriesData>(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/values/timeseries`,
defaultHttpOptionsFromParams(queryParams, config));
}
}

31
ui-ngx/src/app/core/http/http-utils.ts

@ -38,7 +38,10 @@ export function createDefaultHttpOptions(queryParamsOrConfig?: QueryParams | Req
if (hasRequestConfig(queryParamsOrConfig)) {
return defaultHttpOptionsFromConfig(queryParamsOrConfig as RequestConfig);
}
const queryParams = queryParamsOrConfig as QueryParams;
return defaultHttpOptionsFromParams(queryParamsOrConfig as QueryParams, config);
}
export function defaultHttpOptionsFromParams(queryParams?: QueryParams, config?: RequestConfig) {
const finalConfig = {
...config,
...(queryParams && { queryParams }),
@ -57,9 +60,11 @@ export function defaultHttpOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false,
resendRequest: boolean = false,
queryParams?: QueryParams) {
const cleanedParams = cleanQueryParams(queryParams);
return {
headers: new HttpHeaders({'Content-Type': 'application/json'}),
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams)
};
}
@ -67,7 +72,27 @@ export function defaultHttpUploadOptions(ignoreLoading: boolean = false,
ignoreErrors: boolean = false,
resendRequest: boolean = false,
queryParams?: QueryParams) {
const cleanedParams = cleanQueryParams(queryParams);
return {
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), queryParams)
params: new InterceptorHttpParams(new InterceptorConfig(ignoreLoading, ignoreErrors, resendRequest), cleanedParams)
};
}
function cleanQueryParams(params?: QueryParams): QueryParams | undefined {
if (!params) {
return undefined;
}
const entries = Object.entries(params);
const cleanedEntries = entries.filter(
([_, value]) => value !== null && value !== undefined
);
if (!cleanedEntries.length) {
return undefined;
}
return Object.fromEntries(cleanedEntries);
}

2
ui-ngx/src/app/core/http/queue.service.ts

@ -75,7 +75,7 @@ export class QueueService {
}
public getQueueStatisticsByIds(queueStatIds: Array<string>, config?: RequestConfig): Observable<Array<QueueStatisticsInfo>> {
return this.http.get<Array<QueueStatisticsInfo>>(`/api/queueStats?QueueStatsIds=${queueStatIds.join(',')}`,
return this.http.get<Array<QueueStatisticsInfo>>(`/api/queueStats?queueStatsIds=${queueStatIds.join(',')}`,
defaultHttpOptionsFromConfig(config)).pipe(
map(queueStats => queueStats.map(queueStat => this.parseQueueStatName(queueStat))
)

1
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -19,7 +19,6 @@
<mat-toolbar color="primary">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="alarmRule"></div>
<button mat-icon-button
(click)="cancel()"
type="button">

5
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -54,6 +54,7 @@ import {
CalculatedFieldDebugDialogData
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
import { UtilsService } from "@core/services/utils.service";
export class AlarmRulesTableConfig extends EntityTableConfig<any> {
@ -75,6 +76,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
) {
super();
this.tableTitle = this.translate.instant('alarm-rule.alarm-rules');
@ -115,7 +117,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.columns.push(new DateEntityTableColumn<CalculatedFieldAlarmRule>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', '67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));

3
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts

@ -35,6 +35,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
import { UtilsService } from "@core/services/utils.service";
@Component({
selector: 'tb-alarm-rules-table',
@ -63,6 +64,7 @@ export class AlarmRulesTableComponent {
private renderer: Renderer2,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private destroyRef: DestroyRef) {
effect(() => {
@ -80,6 +82,7 @@ export class AlarmRulesTableComponent {
this.ownerId(),
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
);
this.cd.markForCheck();
}

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html

@ -90,7 +90,7 @@
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] mb-[22px] text-center"
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] text-center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html

@ -35,6 +35,7 @@
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts

@ -35,6 +35,7 @@ export interface AlarmRuleComplexFilterPredicateDialogData {
isAdd: boolean;
valueType: EntityKeyValueType;
arguments: Record<string, CalculatedFieldArgument>;
argumentInUse: string;
}
@Component({

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -77,6 +77,7 @@
<tb-alarm-rule-filter-predicate-list [valueType]="filterFormGroup.get('valueType').value"
[operation]="filterFormGroup.get('operation').value"
[arguments]="arguments"
[argumentInUse]="filterFormGroup.get('argument').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -42,6 +42,7 @@
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts

@ -78,6 +78,8 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
@Input() arguments: Record<string, CalculatedFieldArgument>;
@Input() argumentInUse: string;
filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
@ -195,6 +197,7 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
valueType: this.valueType,
isAdd: true,
arguments: this.arguments,
argumentInUse: this.argumentInUse
}
}).afterClosed().pipe(
map(result => result)

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html

@ -55,7 +55,7 @@
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument">{{ argument }}</mat-option>
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option>
}
</mat-select>
@if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) {

12
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts

@ -57,6 +57,9 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
@Input()
valueType: EntityKeyValueType;
@Input()
argumentInUse: string;
valueTypeEnum = EntityKeyValueType;
filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>;
@ -104,7 +107,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
});
this.dynamicModeControl.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => this.updateValueModeValidators(value))
).subscribe(value => this.updateValueModeValidators(value));
}
setDisabledState(isDisabled: boolean): void {
@ -114,16 +117,17 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
} else {
this.filterPredicateValueFormGroup.enable({emitEvent: false});
this.dynamicModeControl.enable({emitEvent: false});
this.updateValueModeValidators(this.dynamicModeControl.value);
}
}
private updateValueModeValidators(isDynamicMode: boolean): void {
if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable();
} else {
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('staticValue').enable();
}
}
@ -142,7 +146,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length);
this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false});
}
private updateModel() {

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html

@ -71,6 +71,7 @@
@if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts

@ -70,6 +70,9 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
argumentInUse: string;
filterPredicateFormGroup = this.fb.group({
operation: [],
ignoreCase: false,
@ -146,6 +149,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
valueType: this.valueType,
isAdd: false,
arguments: this.arguments,
argumentInUse: this.argumentInUse,
}
}).afterClosed().subscribe(
(result) => {

5
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -59,6 +59,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service
import { isObject } from '@core/utils';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
@ -80,6 +81,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
) {
super();
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
@ -124,7 +126,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
};
this.columns.push(new DateEntityTableColumn<CalculatedField>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%'));
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' })));
this.columns.push(expressionColumn);

3
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts

@ -35,6 +35,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
@Component({
selector: 'tb-calculated-fields-table',
@ -63,6 +64,7 @@ export class CalculatedFieldsTableComponent {
private renderer: Renderer2,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private destroyRef: DestroyRef) {
effect(() => {
@ -80,6 +82,7 @@ export class CalculatedFieldsTableComponent {
this.ownerId(),
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
);
this.cd.markForCheck();
}

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss

@ -44,6 +44,12 @@
}
}
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.limit-field-row {
@media screen and (max-width: 520px) {
display: flex;

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss

@ -1,3 +1,18 @@
/**
* 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.
*/
:host ::ng-deep {
.mat-mdc-chip-disabled {
.mdc-evolution-chip__action {

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -94,7 +94,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0;
key.progress = isFinite(progress) ? progress : 0;
key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
});
this.cd.detectChanges();

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.ts

@ -332,11 +332,16 @@ export class LedIndicatorComponent extends PageComponent implements OnInit, OnDe
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.ts

@ -330,11 +330,16 @@ export class RoundSwitchComponent extends PageComponent implements OnInit, OnDes
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

13
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts

@ -373,11 +373,16 @@ export class SwitchComponent extends PageComponent implements AfterViewInit, OnD
if (keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isDefined(attrValue)) {
let parsed = null;
let valueToParse = attrValue;
try {
parsed = this.parseValueFunction(JSON.parse(attrValue));
} catch (e){/**/}
value = !!parsed;
valueToParse = JSON.parse(attrValue);
} catch (e) {/**/}
try {
value = !!this.parseValueFunction(valueToParse);
} catch (e) {
value = false;
}
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -29,7 +29,7 @@ import {
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
ValidatorFn
ValidatorFn, Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -140,7 +140,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) {
this.apiUsageWidgetSettingsForm = this.fb.group({
dsEntityAliasId: [settings?.dsEntityAliasId],
dsEntityAliasId: [settings?.dsEntityAliasId, Validators.required],
apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys),
targetDashboardState: [settings?.targetDashboardState],
background: [settings?.background, []],

2
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -96,7 +96,7 @@
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-center justify-start"
(mousedown)="toggleExtensionPanel($event, i, provider.get('enable').value)"
(mousedown)="toggleExtensionPanel($event, i+1, provider.get('enable').value)"
formControlName="enable">
{{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }}
</mat-slide-toggle>

2
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html

@ -157,7 +157,7 @@
</mat-card-header>
<mat-card-content>
<div class="flex flex-col items-center justify-start">
<form [formGroup]="emailConfigForm" class="mb-8">
<form [formGroup]="emailConfigForm" class="mb-8 w-full">
<p class="mat-body step-description input" translate>login.email-description</p>
<mat-form-field class="mat-block input-container flex-1">
<input matInput formControlName="email"

2
ui-ngx/src/assets/help/en_US/notification/alarm.md

@ -16,11 +16,13 @@ Available template parameters:
* `alarmStatus` - the alarm status;
* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device';
* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorId` - the alarm originator entity id as uuid string;
* `recipientTitle` - title of the recipient (first and last name if specified, email otherwise);
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
* `details.<key>` - any key field from the alarm's details. Fox example, if details are `{"data": "Temperature is 25"}`, use `${details.data}` to access "Temperature is 25";
Parameter names must be wrapped using `${...}`. For example: `${action}`.
You may also modify the value of the parameter with one of the suffixes:

1
ui-ngx/src/assets/help/en_US/notification/alarm_assignment.md

@ -15,6 +15,7 @@ Available template parameters:
* `alarmStatus` - the alarm status;
* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device';
* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorId` - the alarm originator entity id as uuid string;
* `assigneeTitle` - title of the assignee;
* `assigneeEmail` - email of the assignee;

1
ui-ngx/src/assets/help/en_US/notification/alarm_comment.md

@ -15,6 +15,7 @@ Available template parameters:
* `alarmStatus` - the alarm status;
* `alarmOriginatorEntityType` - the entity type of the alarm originator, e.g. 'Device';
* `alarmOriginatorName` - the name of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorLabel` - the label of the alarm originator, e.g. 'Sensor T1';
* `alarmOriginatorId` - the alarm originator entity id as uuid string;
* `comment` - text of the comment;
* `action` - one of: 'added', 'updated';

3
ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md

@ -12,6 +12,9 @@ Available template parameters:
* `edgeId` - the edge id as uuid string;
* `edgeName` - the name of the edge;
* `failureMsg` - the string representation of the failure, occurred on the Edge;
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
Parameter names must be wrapped using `${...}`. For example: `${edgeName}`.
You may also modify the value of the parameter with one of the suffixes:

3
ui-ngx/src/assets/help/en_US/notification/edge_connection.md

@ -12,6 +12,9 @@ Available template parameters:
* `edgeId` - the edge id as uuid string;
* `edgeName` - the name of the edge;
* `eventType` - the string representation of the connectivity status: connected or disconnected;
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
Parameter names must be wrapped using `${...}`. For example: `${edgeName}`.
You may also modify the value of the parameter with one of the suffixes:

3
ui-ngx/src/assets/help/en_US/notification/resources_shortage.md

@ -13,6 +13,9 @@ Available template parameters:
* `usage` - the current usage value of the resource;
* `serviceId` - the service id (convenient in cluster setup);
* `serviceType` - the service type (convenient in cluster setup);
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
Parameter names must be wrapped using `${...}`. For example: `${resource}`.
You may also modify the value of the parameter with one of the suffixes:

3
ui-ngx/src/assets/help/en_US/notification/task_processing_failure.md

@ -16,6 +16,9 @@ Available template parameters:
* `entityType` - the type of the entity to which the task is related;
* `entityId` - the id of the entity to which the task is related;
* `attempt` - the number of attempts processing the task
* `recipientEmail` - email of the recipient;
* `recipientFirstName` - first name of the recipient;
* `recipientLastName` - last name of the recipient;
Parameter names must be wrapped using `${...}`. For example: `${entityType}`.
You may also modify the value of the parameter with one of the suffixes:

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

Loading…
Cancel
Save