Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into feature/dashboard-or-conditions

pull/15286/head
Viacheslav Klimov 3 months ago
parent
commit
69e396d818
Failed to extract signature
  1. 25
      application/src/main/data/upgrade/lts/schema_update.sql
  2. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 6
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 127
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  5. 51
      application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  7. 259
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  8. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  9. 118
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  10. 350
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  11. 4
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  12. 8
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  13. 7
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  14. 14
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  15. 15
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  16. 6
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  18. 126
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  19. 3
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  20. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  21. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  22. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  23. 24
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  25. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  26. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbEdgeConsumerService.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  28. 35
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  29. 4
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractPartitionBasedConsumerService.java
  30. 2
      application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java
  31. 35
      application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java
  32. 14
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  33. 12
      application/src/main/resources/thingsboard.yml
  34. 178
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  35. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  36. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java
  37. 420
      application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java
  38. 73
      application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java
  39. 517
      application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java
  40. 159
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java
  41. 160
      application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java
  42. 277
      application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java
  43. 245
      application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java
  44. 11
      common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java
  45. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  46. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  47. 7
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java
  48. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/AiModel.java
  49. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java
  50. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java
  51. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  52. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java
  53. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java
  54. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java
  55. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java
  56. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java
  57. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java
  58. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java
  59. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java
  60. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java
  61. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java
  62. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java
  63. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java
  64. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java
  66. 7
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java
  67. 166
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java
  68. 39
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java
  69. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java
  70. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java
  71. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java
  72. 9
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java
  73. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java
  74. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java
  75. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java
  76. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java
  77. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  78. 2
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java
  79. 1
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java
  80. 2
      common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java
  81. 3
      common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java
  82. 2
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
  83. 2
      common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java
  84. 12
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  85. 1
      common/proto/src/main/proto/queue.proto
  86. 142
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  87. 4
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
  88. 673
      common/transport/coap/src/main/proto/efento/proto_config.proto
  89. 125
      common/transport/coap/src/main/proto/efento/proto_config_types.proto
  90. 409
      common/transport/coap/src/main/proto/efento/proto_device_info.proto
  91. 185
      common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
  92. 217
      common/transport/coap/src/main/proto/efento/proto_measurements.proto
  93. 407
      common/transport/coap/src/main/proto/efento/proto_rule.proto
  94. 664
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java
  95. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  96. 58
      common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java
  97. 13
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java
  98. 10
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java
  99. 22
      dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java
  100. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java

25
application/src/main/data/upgrade/lts/schema_update.sql

@ -0,0 +1,25 @@
--
-- Copyright © 2016-2026 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.
--
-- LTS cumulative schema update file.
-- All statements must be idempotent (use IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, DO $$ ... END $$ guards, etc.).
-- This file is executed by SystemPatchApplier on every version increase within the LTS family.
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar;
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END

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

@ -36,6 +36,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.SmsService;
@ -691,6 +692,10 @@ public class ActorSystemContext {
@Getter
private MqttClientSettings mqttClientSettings;
@Autowired(required = false)
@Getter
private TbHttpClientSettings tbHttpClientSettings;
@Getter
@Setter
private TbActorSystem actorSystem;

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

@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.DeviceStateManager;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.MqttClientSettings;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
@ -1068,6 +1069,11 @@ public class DefaultTbContext implements TbContext {
return mainCtx.getMqttClientSettings();
}
@Override
public TbHttpClientSettings getTbHttpClientSettings() {
return mainCtx.getTbHttpClientSettings();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());

127
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.config;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
@ -62,12 +63,14 @@ import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardCredentialsExpiredResponse;
import org.thingsboard.server.exception.ThingsboardErrorResponse;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.auth.rest.LoginResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
@ -76,6 +79,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@ -115,6 +119,8 @@ public class SwaggerConfiguration {
// Keyed by the schema name that swagger-core generates (see resolveSchemaName).
private final Map<String, List<String>> schemaPropertyOrders = new ConcurrentHashMap<>();
private final Map<String, Set<String>> schemaOwnProps = new ConcurrentHashMap<>();
// Tracks schema name → fully-qualified class names to detect collisions.
private final Map<String, Set<String>> schemaNameToClasses = new ConcurrentHashMap<>();
@Value("${swagger.api_path:/api/**}")
private String apiPath;
@ -294,6 +300,19 @@ public class SwaggerConfiguration {
@Lazy(false)
ModelConverter mapAwareConverter() {
return (type, context, chain) -> {
// Strip field-level @JsonIgnoreProperties from context annotations so it
// doesn't pollute the global schema. The OpenAPI schema should show all
// properties; field-level ignore is a serialization concern only.
Annotation[] ctxAnnotations = type.getCtxAnnotations();
if (ctxAnnotations != null) {
Annotation[] filtered = Arrays.stream(ctxAnnotations)
.filter(a -> !(a instanceof JsonIgnoreProperties))
.toArray(Annotation[]::new);
if (filtered.length != ctxAnnotations.length) {
type.ctxAnnotations(filtered);
}
}
JavaType javaType = Json.mapper().constructType(type.getType());
if (javaType != null) {
Class<?> cls = javaType.getRawClass();
@ -326,6 +345,10 @@ public class SwaggerConfiguration {
try {
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
String schemaName = resolveSchemaName(javaType);
Set<String> classes = schemaNameToClasses.computeIfAbsent(schemaName, k -> ConcurrentHashMap.newKeySet());
if (classes.add(cls.getName()) && classes.size() > 1) {
log.error("Duplicate OpenAPI schema name '{}' mapped by: {}. Use @Schema(name = ...) to disambiguate.", schemaName, classes);
}
schemaPropertyOrders.put(schemaName, resolvePropertyOrder(cls, beanDesc));
Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
if (!ownProps.isEmpty()) {
@ -355,7 +378,8 @@ public class SwaggerConfiguration {
.addSchemas("LoginResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(LoginResponse.class)).schema)
.addSchemas("ThingsboardErrorResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardErrorResponse.class)).schema)
.addSchemas("ThingsboardCredentialsExpiredResponse", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ThingsboardCredentialsExpiredResponse.class)).schema)
.addSchemas("ThingsboardErrorCode", errorCodeSchema);
.addSchemas("ThingsboardErrorCode", errorCodeSchema)
.addSchemas("AiChatModelConfig", ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(AiChatModelConfig.class)).schema);
}
private OperationCustomizer operationCustomizer() {
@ -372,6 +396,19 @@ public class SwaggerConfiguration {
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME);
return openAPI -> {
// Fail fast on duplicate schema names — two different classes resolving to the same
// OpenAPI schema name causes one to silently overwrite the other.
List<String> duplicates = schemaNameToClasses.entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.map(e -> "'" + e.getKey() + "' mapped by: " + e.getValue())
.sorted()
.toList();
if (!duplicates.isEmpty()) {
throw new IllegalStateException(
"Duplicate OpenAPI schema names detected. Use @Schema(name = ...) to disambiguate:\n "
+ String.join("\n ", duplicates));
}
var paths = openAPI.getPaths();
paths.entrySet().stream()
.peek(entry -> {
@ -407,6 +444,64 @@ public class SwaggerConfiguration {
}
});
// Springdoc creates duplicate schemas with an "Object" suffix when a type is
// resolved through multiple inheritance paths or via generic type resolution.
// Remove the "*Object" duplicate when the base schema exists (either
// pre-registered in addDefaultSchemas or generated by springdoc).
for (String name : new ArrayList<>(schemas.keySet())) {
if (!name.endsWith("Object")) continue;
String baseName = name.substring(0, name.length() - "Object".length());
if (!schemas.containsKey(baseName)) continue;
schemas.remove(name);
String refToRemove = "#/components/schemas/" + name;
schemas.values().forEach(s -> {
if (s.getAllOf() != null) {
s.getAllOf().removeIf(allOfEntry -> refToRemove.equals(((Schema<?>) allOfEntry).get$ref()));
}
});
log.debug("Removed duplicate schema '{}' (base '{}' exists)", name, baseName);
}
// Remove duplicate or redundant inline entries in allOf. Springdoc can
// generate multiple inline property blocks when resolving a type through
// multiple parent paths (e.g. record + sealed interface). One block may be
// a strict subset of another (same properties, but the superset has extras
// like "modelType"). Keep only the superset in that case.
schemas.values().forEach(schema -> {
if (schema.getAllOf() != null && schema.getAllOf().size() > 1) {
List<Schema> allOf = schema.getAllOf();
Set<Integer> redundant = new HashSet<>();
for (int i = 0; i < allOf.size(); i++) {
if (redundant.contains(i)) continue;
Schema a = allOf.get(i);
if (a.get$ref() != null || a.getProperties() == null) continue;
for (int j = i + 1; j < allOf.size(); j++) {
if (redundant.contains(j)) continue;
Schema b = allOf.get(j);
if (b.get$ref() != null || b.getProperties() == null) continue;
if (a.getProperties().entrySet().containsAll(b.getProperties().entrySet())) {
redundant.add(j); // b is a subset of a
} else if (b.getProperties().entrySet().containsAll(a.getProperties().entrySet())) {
redundant.add(i); // a is a subset of b
break;
}
}
}
if (!redundant.isEmpty()) {
List<Schema> filtered = new ArrayList<>();
for (int i = 0; i < allOf.size(); i++) {
if (!redundant.contains(i)) {
filtered.add(allOf.get(i));
}
}
allOf.clear();
allOf.addAll(filtered);
}
}
});
// Fix polymorphic properties: replace inline oneOf with base type $ref
schemas.values().forEach(schema -> {
replaceInlineOneOfProperties(schema, schemas);
@ -546,6 +641,16 @@ public class SwaggerConfiguration {
log.debug("Replaced oneOf in additionalProperties with $ref to {} in property {}", baseType, propName);
}
}
// Check if additionalProperties is an array whose items has oneOf (e.g. Map<K, List<PolymorphicType>>)
if (additionalProps.getItems() != null && additionalProps.getItems().getOneOf() != null && !additionalProps.getItems().getOneOf().isEmpty()) {
String baseType = findBaseTypeForOneOf(allSchemas, additionalProps.getItems().getOneOf());
if (baseType != null) {
Schema<?> refSchema = new Schema<>();
refSchema.set$ref("#/components/schemas/" + baseType);
additionalProps.setItems(refSchema);
log.debug("Replaced oneOf in additionalProperties.items with $ref to {} in property {}", baseType, propName);
}
}
}
// If property has oneOf, try to find the base discriminated type
@ -799,7 +904,13 @@ public class SwaggerConfiguration {
* This matches the naming convention used by swagger-core's {@code TypeNameResolver}.
*/
private static String resolveSchemaName(JavaType javaType) {
StringBuilder sb = new StringBuilder(javaType.getRawClass().getSimpleName());
Class<?> cls = javaType.getRawClass();
io.swagger.v3.oas.annotations.media.Schema schemaAnnotation =
cls.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
if (schemaAnnotation != null && !schemaAnnotation.name().isEmpty()) {
return schemaAnnotation.name();
}
StringBuilder sb = new StringBuilder(cls.getSimpleName());
if (javaType.hasGenericTypes()) {
for (int i = 0; i < javaType.containedTypeCount(); i++) {
JavaType param = javaType.containedType(i);
@ -904,8 +1015,14 @@ public class SwaggerConfiguration {
// Map backing field names to their JSON property names (respects @JsonProperty)
Map<String, String> fieldToJsonName = new LinkedHashMap<>();
for (var prop : beanDesc.findProperties()) {
if (prop.getField() != null && prop.couldSerialize()) {
fieldToJsonName.put(prop.getField().getName(), prop.getName());
if (prop.couldSerialize()) {
if (prop.getField() != null) {
fieldToJsonName.put(prop.getField().getName(), prop.getName());
} else {
// For transient fields, Jackson may not associate the field with the property.
// Fall back to using the property name as the field name key.
fieldToJsonName.putIfAbsent(prop.getName(), prop.getName());
}
}
}
@ -940,4 +1057,4 @@ public class SwaggerConfiguration {
.value(node);
}
}
}

51
application/src/main/java/org/thingsboard/server/config/TbHttpClientSettingsComponent.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2026 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.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.TbHttpClientSettings;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
@TbRuleEngineComponent
@Component
public class TbHttpClientSettingsComponent implements TbHttpClientSettings {
@Value("${actors.rule.external.http_client.max_parallel_requests:0}")
private int maxParallelRequests;
@Value("${actors.rule.external.http_client.max_pending_requests:0}")
private int maxPendingRequests;
@Value("${actors.rule.external.http_client.pool_max_connections:0}")
private int poolMaxConnections;
@Override
public int getMaxParallelRequests() {
return maxParallelRequests;
}
@Override
public int getMaxPendingRequests() {
return maxPendingRequests;
}
@Override
public int getPoolMaxConnections() {
return poolMaxConnections;
}
}

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

@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmCommentId;
import org.thingsboard.server.common.data.id.AlarmId;
@ -77,6 +78,7 @@ public class AlarmCommentController extends BaseController {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
Alarm alarm = checkAlarmInfoId(alarmId, Operation.WRITE);
alarmComment.setAlarmId(alarmId);
alarmComment.setType(AlarmCommentType.OTHER);
return tbAlarmCommentService.saveAlarmComment(alarm, alarmComment, getCurrentUser());
}

259
application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java

@ -0,0 +1,259 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldFilter;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class AlarmRuleController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
public static final String ALARM_RULE_ID = "alarmRuleId";
private static final String TEST_SCRIPT_EXPRESSION =
"Execute the alarm rule TBEL condition expression and return the result. " +
"Alarm rule expressions must return a boolean value. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"expression\": \"return temperature > 50;\",\n" +
" \"arguments\": {\n" +
" \"temperature\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 55 }\n" +
" }\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n Expected result JSON contains \"output\" and \"error\".";
@ApiOperation(value = "Create Or Update Alarm Rule (saveAlarmRule)",
notes = "Creates or Updates the Alarm Rule. When creating alarm rule, platform generates Alarm Rule Id as " + UUID_WIKI_LINK +
"The newly created Alarm Rule Id will be present in the response. " +
"Specify existing Alarm Rule Id to update the alarm rule. " +
"Referencing non-existing Alarm Rule Id will cause 'Not Found' error. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Alarm Rule entity. "
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/alarm/rule")
public AlarmRuleDefinition saveAlarmRule(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm rule.")
@RequestBody AlarmRuleDefinition alarmRuleDefinition) throws Exception {
alarmRuleDefinition.setTenantId(getTenantId());
checkEntityId(alarmRuleDefinition.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
if (alarmRuleDefinition.getId() != null) {
checkAlarmRule(alarmRuleDefinition.getId());
}
CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField();
checkReferencedEntities(calculatedField.getConfiguration());
CalculatedField saved = tbCalculatedFieldService.save(calculatedField, getCurrentUser());
return AlarmRuleDefinition.fromCalculatedField(saved);
}
@ApiOperation(value = "Get Alarm Rule (getAlarmRuleById)",
notes = "Fetch the Alarm Rule object based on the provided Alarm Rule Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/alarm/rule/{alarmRuleId}")
public AlarmRuleDefinition getAlarmRuleById(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
return AlarmRuleDefinition.fromCalculatedField(calculatedField);
}
@ApiOperation(value = "Get Alarm Rules by Entity Id (getAlarmRulesByEntityId)",
notes = "Fetch the Alarm Rules based on the provided Entity Id." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules/{entityType}/{entityId}")
public PageData<AlarmRuleDefinition> getAlarmRulesByEntityId(
@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 = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"})) @RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
checkParameter("entityId", entityIdStr);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr);
checkEntityId(entityId, Operation.READ_CALCULATED_FIELD);
PageData<CalculatedField> result = checkNotNull(tbCalculatedFieldService.findByTenantIdAndEntityId(getTenantId(), entityId, CalculatedFieldType.ALARM, pageLink));
return result.mapData(AlarmRuleDefinition::fromCalculatedField);
}
@ApiOperation(value = "Get alarm rules (getAlarmRules)",
notes = "Fetch tenant alarm rules based on the filter." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules")
public PageData<AlarmRuleDefinitionInfo> getAlarmRules(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = "Entity type filter. If not specified, alarm rules for all supported entity types will be returned.")
@RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter. If not specified, alarm rules for entity type filter will be returned.")
@RequestParam(required = false) Set<UUID> entities,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
SecurityUser user = getCurrentUser();
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream()
.filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
} else {
entityTypes = EnumSet.of(entityType);
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.types(EnumSet.of(CalculatedFieldType.ALARM))
.entityTypes(entityTypes)
.entityIds(entities)
.build();
PageData<CalculatedFieldInfo> result = calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink);
return result.mapData(AlarmRuleDefinitionInfo::fromCalculatedFieldInfo);
}
@ApiOperation(value = "Get alarm rule names (getAlarmRuleNames)",
notes = "Fetch the list of alarm rule names." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rules/names")
public PageData<String> getAlarmRuleNames(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = CF_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, "name", sortOrder);
return calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType(getTenantId(), CalculatedFieldType.ALARM, pageLink);
}
@ApiOperation(value = "Delete Alarm Rule (deleteAlarmRule)",
notes = "Deletes the alarm rule. Referencing non-existing Alarm Rule Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@DeleteMapping("/alarm/rule/{alarmRuleId}")
@ResponseStatus(HttpStatus.OK)
public void deleteAlarmRule(@PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws Exception {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
tbCalculatedFieldService.delete(calculatedField, getCurrentUser());
}
@ApiOperation(value = "Get latest alarm rule debug event (getLatestAlarmRuleDebugEvent)",
notes = "Gets latest alarm rule debug event for specified alarm rule id. " +
"Referencing non-existing alarm rule id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping("/alarm/rule/{alarmRuleId}/debug")
public JsonNode getLatestAlarmRuleDebugEvent(@Parameter @PathVariable(ALARM_RULE_ID) String strAlarmRuleId) throws ThingsboardException {
checkParameter(ALARM_RULE_ID, strAlarmRuleId);
CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strAlarmRuleId));
CalculatedField calculatedField = checkAlarmRule(calculatedFieldId);
checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD);
TenantId tenantId = getCurrentUser().getTenantId();
return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1))
.flatMap(events -> events.stream().map(EventInfo::getBody).findFirst())
.orElse(null);
}
@ApiOperation(value = "Test alarm rule TBEL expression (testAlarmRuleScript)",
notes = TEST_SCRIPT_EXPRESSION + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@PostMapping("/alarm/rule/testScript")
public JsonNode testAlarmRuleScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test alarm rule TBEL condition expression. The expression must return a boolean value.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null);
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
private CalculatedField checkAlarmRule(CalculatedFieldId calculatedFieldId) throws ThingsboardException {
CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkNotNull(calculatedField);
if (calculatedField.getType() != CalculatedFieldType.ALARM) {
throw new ThingsboardException("Alarm rule not found", ThingsboardErrorCode.ITEM_NOT_FOUND);
}
return calculatedField;
}
}

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

@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.domain.Domain;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeInfo;
@ -681,6 +682,17 @@ public abstract class BaseController {
return entity;
}
protected void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
for (EntityId referencedEntityId : calculatedFieldConfig.getReferencedEntities()) {
EntityType refEntityType = referencedEntityId.getEntityType();
switch (refEntityType) {
case TENANT -> {}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Unsupported referenced entity type: '" + refEntityType + "'.");
}
}
}
Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException {
return checkEntityId(deviceId, deviceService::findDeviceById, operation);
}

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

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
@ -24,10 +23,7 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
@ -40,13 +36,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -65,21 +54,15 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.controller.ControllerConstants.CF_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
@ -98,17 +81,13 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class CalculatedFieldController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String CALCULATED_FIELD_ID = "calculatedFieldId";
public static final int TIMEOUT = 20;
private static final String TEST_SCRIPT_EXPRESSION =
"Execute the Script expression and return the result. The format of request: \n\n"
+ MARKDOWN_CODE_BLOCK_START
@ -171,13 +150,13 @@ public class CalculatedFieldController extends BaseController {
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@GetMapping(value = "/{entityType}/{entityId}/calculatedFields", params = {"pageSize", "page"})
public PageData<CalculatedField> getCalculatedFieldsByEntityIdV1(@PathVariable("entityType") String entityType,
@PathVariable("entityId") String entityIdStr,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) CalculatedFieldType type,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@PathVariable("entityId") String entityIdStr,
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) CalculatedFieldType type,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
checkParameter("entityId", entityIdStr);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityIdStr);
@ -305,86 +284,9 @@ public class CalculatedFieldController extends BaseController {
@PostMapping("/calculatedField/testScript")
public JsonNode testCalculatedFieldScript(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Test calculated field TBEL expression.")
@RequestBody JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
getTenantId(),
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ExceptionUtils.getRootCause(e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
}
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
Set<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType entityType = referencedEntityId.getEntityType();
switch (entityType) {
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
}
}
@RequestBody JsonNode inputParams) throws ThingsboardException {
checkParameter("expression", inputParams.has("expression") ? inputParams.get("expression").asText() : null);
return tbCalculatedFieldService.executeTestScript(getTenantId(), inputParams);
}
}

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

@ -444,106 +444,6 @@ public class ControllerConstants {
" * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
" * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":true,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempAttr\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END;
protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value.";
protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'";
@ -1328,8 +1228,6 @@ public class ControllerConstants {
ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE;
protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
@ -1343,219 +1241,6 @@ public class ControllerConstants {
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"alarms\":[\n" +
" {\n" +
" \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" +
" \"alarmType\":\"Temperature Alarm\",\n" +
" \"clearRule\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"propagate\":false,\n" +
" \"createRules\":{\n" +
" \"MAJOR\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"SPECIFIC_TIME\",\n" +
" \"endsOn\":64800000,\n" +
" \"startsOn\":43200000,\n" +
" \"timezone\":\"Europe/Kiev\",\n" +
" \"daysOfWeek\":[\n" +
" 1,\n" +
" 3,\n" +
" 5\n" +
" ]\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"DURATION\",\n" +
" \"unit\":\"MINUTES\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"COMPLEX\",\n" +
" \"operation\":\"OR\",\n" +
" \"predicates\":[\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"LESS_OR_EQUAL\"\n" +
" },\n" +
" {\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":30.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"WARNING\":{\n" +
" \"schedule\":{\n" +
" \"type\":\"CUSTOM\",\n" +
" \"items\":[\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":1\n" +
" },\n" +
" {\n" +
" \"endsOn\":64800000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":43200000,\n" +
" \"dayOfWeek\":2\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":3\n" +
" },\n" +
" {\n" +
" \"endsOn\":57600000,\n" +
" \"enabled\":true,\n" +
" \"startsOn\":36000000,\n" +
" \"dayOfWeek\":4\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":5\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":6\n" +
" },\n" +
" {\n" +
" \"endsOn\":0,\n" +
" \"enabled\":false,\n" +
" \"startsOn\":0,\n" +
" \"dayOfWeek\":7\n" +
" }\n" +
" ],\n" +
" \"timezone\":\"Europe/Kiev\"\n" +
" },\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"REPEATING\",\n" +
" \"predicate\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":5,\n" +
" \"dynamicValue\":null\n" +
" }\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"tempConstant\",\n" +
" \"type\":\"CONSTANT\"\n" +
" },\n" +
" \"value\":30,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":0.0,\n" +
" \"dynamicValue\":{\n" +
" \"inherit\":false,\n" +
" \"sourceType\":\"CURRENT_DEVICE\",\n" +
" \"sourceAttribute\":\"tempThreshold\"\n" +
" }\n" +
" },\n" +
" \"operation\":\"EQUAL\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" },\n" +
" \"CRITICAL\":{\n" +
" \"schedule\":null,\n" +
" \"condition\":{\n" +
" \"spec\":{\n" +
" \"type\":\"SIMPLE\"\n" +
" },\n" +
" \"condition\":[\n" +
" {\n" +
" \"key\":{\n" +
" \"key\":\"temperature\",\n" +
" \"type\":\"TIME_SERIES\"\n" +
" },\n" +
" \"value\":null,\n" +
" \"predicate\":{\n" +
" \"type\":\"NUMERIC\",\n" +
" \"value\":{\n" +
" \"userValue\":null,\n" +
" \"defaultValue\":50.0,\n" +
" \"dynamicValue\":null\n" +
" },\n" +
" \"operation\":\"GREATER\"\n" +
" },\n" +
" \"valueType\":\"NUMERIC\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"dashboardId\":null,\n" +
" \"alarmDetails\":null\n" +
" }\n" +
" },\n" +
" \"propagateRelationTypes\":null\n" +
" }\n" +
" ],\n" +
" \"configuration\":{\n" +
" \"type\":\"DEFAULT\"\n" +
" },\n" +
@ -1577,40 +1262,11 @@ public class ControllerConstants {
" }\n" +
"}" + MARKDOWN_CODE_BLOCK_END;
protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE +
"Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
"Device profile data object contains device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
"First one is the default device profile data configuration and second one - the custom one. " +
NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE +
NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:";
protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE +
"Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " +
"**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " +
"**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE +
"## Specific Time Schedule" + NEW_LINE +
DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE +
"## Custom Schedule" +
NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE;
protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE +
"Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE +
"Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " +
"or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" +
"\n**'sourceType'** of the **'sourceAttribute'** can be: \n" +
" * 'CURRENT_DEVICE';\n" +
" * 'CURRENT_CUSTOMER';\n" +
" * 'CURRENT_TENANT'." + NEW_LINE +
"**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE +
"## Repeating alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE +
"## Duration alarm condition" + NEW_LINE +
DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE +
"**'unit'** can be: \n" +
" * 'SECONDS';\n" +
" * 'MINUTES';\n" +
" * 'HOURS';\n" +
" * 'DAYS'." + NEW_LINE;
protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE +
"There are 3 types of device provision configuration for the device profile: \n" +
" * 'DISABLED';\n" +
@ -1618,8 +1274,8 @@ public class ControllerConstants {
" * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE +
"Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE;
protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE +
KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION +
PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
protected static final String DEVICE_PROFILE_ID = "deviceProfileId";

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

@ -151,6 +151,8 @@ public class DashboardController extends BaseController {
@ApiOperation(value = "Get Dashboard (getDashboardById)",
notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = Dashboard.class)))
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/{dashboardId}")
public void getDashboardById(@Parameter(description = DASHBOARD_ID_PARAM_DESCRIPTION)
@ -415,6 +417,8 @@ public class DashboardController extends BaseController {
"If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " +
"If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. "
+ DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = HomeDashboard.class)))
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@GetMapping(value = "/dashboard/home")
public void getHomeDashboard(@RequestHeader(name = HttpHeaders.ACCEPT_ENCODING, required = false) String acceptEncodingHeader,

8
application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java

@ -16,6 +16,8 @@
package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@ -50,7 +52,8 @@ public class RpcV1Controller extends AbstractRpcController {
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}
@ -62,7 +65,8 @@ public class RpcV1Controller extends AbstractRpcController {
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV1(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT);
}

7
application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ -127,7 +128,8 @@ public class RpcV2Controller extends AbstractRpcController {
public DeferredResult<ResponseEntity> handleOneWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}
@ -146,7 +148,8 @@ public class RpcV2Controller extends AbstractRpcController {
public DeferredResult<ResponseEntity> handleTwoWayDeviceRPCRequestV2(
@Parameter(description = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String deviceIdStr,
@Parameter(description = "A JSON value representing the RPC request.")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the RPC request.",
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
}

14
application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java

@ -17,6 +17,8 @@ package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -87,7 +89,8 @@ public class RuleEngineController extends BaseController {
@RequestMapping(value = "/", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> handleRuleEngineRequestForUser(
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequestForEntityWithQueueAndTimeout(null, null, null, defaultResponseTimeout, requestBody);
}
@ -106,7 +109,8 @@ public class RuleEngineController extends BaseController {
@PathVariable("entityType") String entityType,
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String entityIdStr,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, defaultResponseTimeout, requestBody);
}
@ -127,7 +131,8 @@ public class RuleEngineController extends BaseController {
@PathVariable("entityId") String entityIdStr,
@Parameter(description = "Timeout to process the request in milliseconds", required = true)
@PathVariable("timeout") int timeout,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
return handleRuleEngineRequestForEntityWithQueueAndTimeout(entityType, entityIdStr, null, timeout, requestBody);
}
@ -151,7 +156,8 @@ public class RuleEngineController extends BaseController {
@PathVariable("queueName") String queueName,
@Parameter(description = "Timeout to process the request in milliseconds", required = true)
@PathVariable("timeout") int timeout,
@Parameter(description = "A JSON value representing the message.", required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON object representing the message.", required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String requestBody) throws ThingsboardException {
try {
SecurityUser currentUser = getCurrentUser();

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

@ -386,7 +386,8 @@ public class TelemetryController extends BaseController {
@PathVariable("deviceId") String deviceIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -411,7 +412,8 @@ public class TelemetryController extends BaseController {
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -436,7 +438,8 @@ public class TelemetryController extends BaseController {
@PathVariable("entityId") String entityIdStr,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE"}, requiredMode = Schema.RequiredMode.REQUIRED))
@PathVariable("scope") AttributeScope scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true)
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string")))
@RequestBody String request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
@ -460,7 +463,8 @@ 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 = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, 0L);
}
@ -484,7 +488,8 @@ public class TelemetryController extends BaseController {
@Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@Parameter(description = TELEMETRY_SCOPE_DESCRIPTION, required = true, schema = @Schema(allowableValues = "ANY")) @PathVariable("scope") String scope,
@Parameter(description = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl,
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true,
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string"))) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, ttl);
}

6
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -118,7 +118,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
private final ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
private Cache<String, SessionMetaData> pendingSessions;
@ -631,7 +631,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0
&& UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser();
if (limitAllowed) {
@ -675,7 +675,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
}
if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.remove(sessionId);
}

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

@ -61,7 +61,7 @@ public interface CalculatedFieldCache {
void addOwnerEntity(TenantId tenantId, EntityId entityId);
void evictEntity(EntityId entityId);
void evictOwnerEntity(EntityId entityId);
void evictOwner(EntityId owner);

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

@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.server.actors.ActorSystemContext;
@ -37,6 +38,8 @@ 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.page.PageDataIterable;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.AfterStartUp;
@ -45,7 +48,9 @@ import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -53,6 +58,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@ -268,18 +274,25 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
@Override
public void updateOwnerEntity(TenantId tenantId, EntityId entityId) {
evictEntity(entityId);
evictOwnerEntity(entityId);
addOwnerEntity(tenantId, entityId);
}
@Override
public void evictEntity(EntityId entityId) {
public void evictOwnerEntity(EntityId entityId) {
ownerEntities.values().forEach(entities -> entities.remove(entityId));
}
@Override
public void evictOwner(EntityId owner) {
ownerEntities.remove(owner);
Set<EntityId> removedEntities = ownerEntities.remove(owner);
if (removedEntities != null) {
Set<EntityId> removedCustomers = removedEntities
.stream()
.filter(entityId -> entityId.getEntityType() == EntityType.CUSTOMER)
.collect(Collectors.toSet());
removedCustomers.forEach(this::evictOwner);
}
}
private Set<EntityId> getOwnedEntities(TenantId tenantId, EntityId ownerId) {
@ -290,6 +303,113 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
});
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
TenantProfileId tenantProfileId = new TenantProfileId(event.getEntityId().getId());
handleTenantProfileUpdate(tenantProfileId);
}
break;
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
evictTenantCfs(tenantId);
evictOwner(tenantId);
}
break;
case CUSTOMER:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwner(event.getEntityId());
evictOwnerEntity(event.getEntityId());
}
break;
case DEVICE, ASSET:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED && event.isOwnerChanged()) {
updateOwnerEntity(event.getTenantId(), event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictOwnerEntity(event.getEntityId());
evictEntityCfs(event.getEntityId());
}
break;
case DEVICE_PROFILE, ASSET_PROFILE:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
evictEntityCfs(event.getEntityId());
}
break;
case CALCULATED_FIELD:
if (event.getEvent() == ComponentLifecycleEvent.CREATED) {
addCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else if (event.getEvent() == ComponentLifecycleEvent.UPDATED) {
updateCalculatedField(event.getTenantId(), (CalculatedFieldId) event.getEntityId());
} else {
evict((CalculatedFieldId) event.getEntityId());
}
break;
}
}
private void evictTenantCfs(TenantId tenantId) {
var removedCfEntityIds = new HashSet<EntityId>();
var removedLinkEntityIds = new HashSet<EntityId>();
var toRemove = calculatedFields.entrySet().stream()
.filter(e -> e.getValue().getTenantId().equals(tenantId))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
toRemove.forEach(cfId -> {
CalculatedField cf = calculatedFields.remove(cfId);
List<CalculatedFieldLink> links = calculatedFieldLinks.remove(cfId);
if (links != null) {
links.forEach(link -> removedLinkEntityIds.add(link.entityId()));
}
calculatedFieldsCtx.remove(cfId);
if (cf != null) {
removedCfEntityIds.add(cf.getEntityId());
}
});
removedCfEntityIds.forEach(entityId -> {
entityIdCalculatedFields.compute(entityId, (k, cfs) -> {
if (cfs != null) {
cfs.removeIf(cf -> toRemove.contains(cf.getId()));
return cfs.isEmpty() ? null : cfs;
}
return null;
});
});
removedLinkEntityIds.forEach(entityId -> {
entityIdCalculatedFieldLinks.compute(entityId, ((entityId1, links) -> {
if (links != null) {
links.removeIf(link -> toRemove.contains(link.calculatedFieldId()));
return links.isEmpty() ? null : links;
}
return null;
}));
});
}
private void evictEntityCfs(EntityId entityId) {
List<CalculatedField> cfs = entityIdCalculatedFields.remove(entityId);
if (cfs != null) {
var cfIds = new HashSet<CalculatedFieldId>();
cfs.forEach(cf -> {
calculatedFields.remove(cf.getId());
calculatedFieldLinks.remove(cf.getId());
calculatedFieldsCtx.remove(cf.getId());
cfIds.add(cf.getId());
log.debug("[{}] evict calculated field from cache on entity deletion: {}", cf.getId(), cf);
});
entityIdCalculatedFieldLinks.values().forEach(list -> list.removeIf(link -> cfIds.contains(link.calculatedFieldId())));
}
entityIdCalculatedFieldLinks.remove(entityId);
}
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}

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

@ -305,7 +305,8 @@ public class CalculatedFieldCtx implements Closeable {
public void setTenantProfileProperties() {
TenantProfile tenantProfile = systemContext.getTenantProfileCache().get(tenantId);
if (tenantProfile == null) {
throw new IllegalStateException("Tenant Profile not found for tenant: " + tenantId);
log.warn("[{}][{}][{}] Tenant Profile not found for tenant: {}. CF limits and thresholds will not be updated.", tenantId, entityId, cfId, tenantId);
return;
}
tenantProfile.getProfileConfiguration().ifPresent(config -> {
this.maxStateSize = config.getMaxStateSizeInKBytes() * 1024L;

92
application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java

@ -15,10 +15,22 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelCfTsDoubleVal;
import org.thingsboard.script.api.tbel.TbelCfTsRollingArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -31,10 +43,16 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngine;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@TbCoreComponent
@Service
@ -42,8 +60,13 @@ import java.util.Set;
@RequiredArgsConstructor
public class DefaultTbCalculatedFieldService extends AbstractTbEntityService implements TbCalculatedFieldService {
private static final int TIMEOUT = 20;
private final CalculatedFieldService calculatedFieldService;
@Autowired(required = false)
private TbelInvokeService tbelInvokeService;
@Override
public CalculatedField save(CalculatedField calculatedField, SecurityUser user) throws ThingsboardException {
ActionType actionType = calculatedField.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
@ -89,6 +112,75 @@ public class DefaultTbCalculatedFieldService extends AbstractTbEntityService imp
}
}
@Override
public JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams) {
String expression = inputParams.get("expression").asText();
Map<String, TbelCfArg> arguments = Objects.requireNonNullElse(
JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}),
Collections.emptyMap()
);
ArrayList<String> ctxAndArgNames = new ArrayList<>(arguments.size() + 1);
ctxAndArgNames.add("ctx");
ctxAndArgNames.addAll(arguments.keySet());
String output = "";
String errorText = "";
CalculatedFieldTbelScriptEngine engine = null;
try {
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new CalculatedFieldTbelScriptEngine(
tenantId,
tbelInvokeService,
expression,
ctxAndArgNames.toArray(String[]::new)
);
Object[] args = new Object[ctxAndArgNames.size()];
args[0] = new TbelCfCtx(arguments, getLatestTimestamp(arguments));
for (int i = 1; i < ctxAndArgNames.size(); i++) {
var arg = arguments.get(ctxAndArgNames.get(i));
if (arg instanceof TbelCfSingleValueArg svArg) {
args[i] = svArg.getValue();
} else {
args[i] = arg;
}
}
JsonNode json = engine.executeJsonAsync(args).get(TIMEOUT, TimeUnit.SECONDS);
output = JacksonUtil.toString(json);
} catch (Exception e) {
log.error("Error evaluating expression", e);
Throwable rootCause = ObjectUtils.firstNonNull(ExceptionUtils.getRootCause(e), e);
errorText = ObjectUtils.firstNonNull(rootCause.getMessage(), e.getClass().getSimpleName());
} finally {
if (engine != null) {
engine.destroy();
}
}
return JacksonUtil.newObjectNode()
.put("output", output)
.put("error", errorText);
}
private static long getLatestTimestamp(Map<String, TbelCfArg> arguments) {
long lastUpdateTimestamp = -1;
for (TbelCfArg entry : arguments.values()) {
if (entry instanceof TbelCfSingleValueArg singleValueArg) {
long ts = singleValueArg.getTs();
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, ts);
} else if (entry instanceof TbelCfTsRollingArg tsRollingArg) {
long maxTs = tsRollingArg.getValues().stream().mapToLong(TbelCfTsDoubleVal::getTs).max().orElse(-1);
lastUpdateTimestamp = Math.max(lastUpdateTimestamp, maxTs);
}
}
return lastUpdateTimestamp == -1 ? System.currentTimeMillis() : lastUpdateTimestamp;
}
private void checkForEntityChange(CalculatedField oldCalculatedField, CalculatedField newCalculatedField) {
if (!oldCalculatedField.getEntityId().equals(newCalculatedField.getEntityId())) {
throw new IllegalArgumentException("Changing the calculated field target entity after initialization is prohibited.");

3
application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.entitiy.cf;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -35,4 +36,6 @@ public interface TbCalculatedFieldService {
void delete(CalculatedField calculatedField, SecurityUser user);
JsonNode executeTestScript(TenantId tenantId, JsonNode inputParams);
}

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<AssetProfileId> toRemove = assetProfilesMap.values().stream()
.filter(assetProfile -> assetProfile.getTenantId().equals(tenantId))
.map(AssetProfile::getId)
.collect(Collectors.toSet());
assetProfilesMap.keySet().removeAll(toRemove);
assetsMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
assetProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

24
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -23,15 +24,19 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@Service
@Slf4j
@ -143,6 +148,25 @@ public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
}
}
@EventListener(ComponentLifecycleMsg.class)
public void onComponentLifecycleEvent(ComponentLifecycleMsg event) {
switch (event.getEntityId().getEntityType()) {
case TENANT:
if (event.getEvent() == ComponentLifecycleEvent.DELETED) {
TenantId tenantId = event.getTenantId();
Set<DeviceProfileId> toRemove = deviceProfilesMap.values().stream()
.filter(deviceProfile -> deviceProfile.getTenantId().equals(tenantId))
.map(DeviceProfile::getId)
.collect(Collectors.toSet());
deviceProfilesMap.keySet().removeAll(toRemove);
devicesMap.entrySet().removeIf(entry -> toRemove.contains(entry.getValue()));
profileListeners.remove(tenantId);
deviceProfileListeners.remove(tenantId);
}
break;
}
}
private void notifyProfileListeners(DeviceProfile profile) {
ConcurrentMap<EntityId, Consumer<DeviceProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {

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

@ -51,7 +51,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.cf.CalculatedFieldStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@ -91,9 +90,8 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
CalculatedFieldStateService stateService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.queueFactory = tbQueueFactory;
this.stateService = stateService;

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

@ -86,7 +86,6 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.notification.NotificationSchedulerService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
@ -179,9 +178,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbImageService imageService,
TbResourceDataCache tbResourceDataCache,
RuleEngineCallService ruleEngineCallService,
CalculatedFieldCache calculatedFieldCache,
EdqsService edqsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService,
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService,
eventPublisher, jwtSettingsService);
this.stateService = stateService;
this.localSubscriptionService = localSubscriptionService;

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

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

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

@ -46,7 +46,6 @@ import org.thingsboard.server.queue.discovery.QueueKey;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedConsumerService;
@ -86,9 +85,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractPartitionBasedCo
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService,
CalculatedFieldCache calculatedFieldCache,
TbMsgPackProcessingContextFactory packProcessingContextFactory) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
this.ctx = ctx;
this.tbDeviceRpcService = tbDeviceRpcService;
this.queueService = queueService;

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

@ -26,7 +26,6 @@ import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -47,7 +46,6 @@ import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
@ -75,7 +73,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbResourceDataCache tbResourceDataCache;
protected final CalculatedFieldCache calculatedFieldCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
protected final ApplicationEventPublisher eventPublisher;
@ -166,7 +163,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
tenantProfileCache.evict(tenantProfileId);
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
calculatedFieldCache.handleTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (TenantId.SYS_TENANT_ID.equals(tenantId)) {
@ -179,7 +175,6 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantUpdate(tenantId);
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete(tenantId);
calculatedFieldCache.evictOwner(tenantId);
partitionService.removeTenant(tenantId);
}
}
@ -187,45 +182,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
deviceProfileCache.evict(tenantId, new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(tenantId, new DeviceId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(tenantId, new AssetId(componentLifecycleMsg.getEntityId().getId()));
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
apiUsageStateService.onApiUsageStateUpdate(tenantId);
} else if (EntityType.CUSTOMER.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.CREATED)) {
calculatedFieldCache.addOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED) && componentLifecycleMsg.isOwnerChanged()) {
calculatedFieldCache.updateOwnerEntity(tenantId, componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.DELETED) {
apiUsageStateService.onCustomerDelete((CustomerId) componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictOwner(componentLifecycleMsg.getEntityId());
calculatedFieldCache.evictEntity(componentLifecycleMsg.getEntityId());
}
} else if (EntityType.CALCULATED_FIELD.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.CREATED) {
calculatedFieldCache.addCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else if (componentLifecycleMsg.getEvent() == ComponentLifecycleEvent.UPDATED) {
calculatedFieldCache.updateCalculatedField(tenantId, (CalculatedFieldId) componentLifecycleMsg.getEntityId());
} else {
calculatedFieldCache.evict((CalculatedFieldId) componentLifecycleMsg.getEntityId());
}
} else if (EntityType.TB_RESOURCE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tbResourceDataCache.evictResourceData(tenantId, new TbResourceId(componentLifecycleMsg.getEntityId().getId()));

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

@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -45,12 +44,11 @@ public abstract class AbstractPartitionBasedConsumerService<N extends com.google
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbResourceDataCache tbResourceDataCache,
CalculatedFieldCache calculatedFieldCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService,
ApplicationEventPublisher eventPublisher,
JwtSettingsService jwtSettingsService) {
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService);
}
@PostConstruct

2
application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java

@ -73,7 +73,7 @@ public class DefaultRuleEngineCallService implements RuleEngineCallService {
UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB());
Consumer<TbMsg> consumer = requests.remove(requestId);
if (consumer != null) {
consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), restApiCallResponseMsg.getResponse(), TbMsgCallback.EMPTY));
consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), TbMsgCallback.EMPTY));
} else {
log.trace("[{}] Unknown or stale rest api call response received", requestId);
}

35
application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java

@ -40,6 +40,7 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -48,7 +49,7 @@ import java.util.stream.Stream;
/**
* Runs at application startup and applies no-downtime data updates
* when the package PATCH version increases (e.g., 4.2.1.0 -> 4.2.1.1).
* when the package version increases within the same LTS family (e.g., 4.3.0.0 -> 4.3.1.0 or 4.3.0.0 -> 4.3.0.1).
*/
@Slf4j
@Component
@ -91,6 +92,8 @@ public class SystemPatchApplier {
}
try {
updateLtsSqlSchema();
updateSqlViews();
log.info("Updated sql database views");
@ -119,17 +122,37 @@ public class SystemPatchApplier {
return false;
}
if (!isPatchVersionChanged(packageVersionInfo, dbVersionInfo)) {
if (!isVersionIncreased(packageVersionInfo, dbVersionInfo)) {
return false;
}
log.info("Patch version increased from {} to {}. Starting system data update.", dbVersion, packageVersion);
log.info("Version increased from {} to {}. Starting system data update.", dbVersion, packageVersion);
return true;
}
private boolean isPatchVersionChanged(VersionInfo packageVersion, VersionInfo dbVersion) {
return packageVersion.major == dbVersion.major && packageVersion.minor == dbVersion.minor
&& packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch;
private boolean isVersionIncreased(VersionInfo packageVersion, VersionInfo dbVersion) {
if (packageVersion.major != dbVersion.major || packageVersion.minor != dbVersion.minor) {
return false;
}
if (packageVersion.maintenance != dbVersion.maintenance) {
return packageVersion.maintenance > dbVersion.maintenance;
}
return packageVersion.patch > dbVersion.patch;
}
private void updateLtsSqlSchema() {
Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", "schema_update.sql");
if (!Files.exists(sqlFile)) {
log.trace("LTS schema update file does not exist: {}", sqlFile);
return;
}
try {
String sql = Files.readString(sqlFile);
jdbcTemplate.execute(sql);
log.info("Applied LTS SQL schema update from {}", sqlFile);
} catch (IOException e) {
throw new RuntimeException("Failed to read LTS schema update file: " + sqlFile, e);
}
}
private void updateSqlViews() {

14
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -144,7 +144,7 @@ public class DefaultWebSocketService implements WebSocketService {
private final ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Map<Integer, Integer>> sessionCmdMap = new ConcurrentHashMap<>();
private ExecutorService executor;
@ -315,7 +315,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
private void processSessionClose(WebSocketSessionRef sessionRef) {
void processSessionClose(WebSocketSessionRef sessionRef) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration != null) {
String sessionId = "[" + sessionRef.getSessionId() + "]";
@ -340,7 +340,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
publicUserSessions.removeIf(subId -> subId.startsWith(sessionId));
}
@ -349,7 +349,7 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
private boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
boolean processSubscription(WebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
var tenantProfileConfiguration = getTenantProfileConfiguration(sessionRef);
if (tenantProfileConfiguration == null) return true;
@ -401,9 +401,11 @@ public class DefaultWebSocketService implements WebSocketService {
}
}
if (tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
synchronized (publicUserSessions) {
if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) {
if (cmd.isUnsubscribe()) {
publicUserSessions.remove(subId);
} else if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSubscriptionsPerPublicUser()) {
publicUserSessions.add(subId);
} else {
log.info("[{}][{}][{}] Failed to start subscription. Max public user subscriptions limit reached"

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

@ -598,6 +598,18 @@ actors:
# Use this when your rule chains need to reach devices on private networks (e.g., 192.168.1.0/24).
# Example: "192.168.1.0/24,10.0.0.0/8,my-internal-service.corp"
ssrf_allowed_hosts: "${SSRF_ALLOWED_HOSTS:}"
http_client:
# Server-level ceiling for parallel in-flight HTTP requests per external HTTP rule node instance.
# Applied as min(nodeConfig, systemMax) when set; 0 = no system-level restriction (node config wins).
max_parallel_requests: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_MAX_PARALLEL_REQUESTS:0}"
# Server-level ceiling for the pending-request queue depth per external HTTP rule node instance.
# Applied as min(nodeConfig, systemMax) when set; 0 = no system-level restriction.
max_pending_requests: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_MAX_PENDING_REQUESTS:0}"
# Maximum number of TCP connections in the reactor-netty connection pool per external HTTP rule node instance.
# Defaults to reactor-netty's ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS: max(availableProcessors, 8) * 2
# (e.g. 16 on an 8-core host). Increase for high-throughput nodes calling remote services that support many connections.
# 0 = use reactor-netty default.
pool_max_connections: "${ACTORS_RULE_EXTERNAL_HTTP_CLIENT_POOL_MAX_CONNECTIONS:${TB_RE_HTTP_CLIENT_POOL_MAX_CONNECTIONS:0}}"
rpc:
# Maximum number of persistent RPC call retries in case of failed request delivery.
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"

178
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -48,8 +48,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate.StringOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
@ -128,32 +127,32 @@ public class AlarmRulesTest extends AbstractControllerTest {
);
Condition clearRule = new Condition("return temperature <= 25;", null, null);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, clearRule);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":100}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":101}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":20}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
@ -185,11 +184,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition(simpleExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":100}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -209,11 +208,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}"));
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -236,15 +235,15 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
for (int i = 0; i < 4; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -255,12 +254,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> alarmResult.getConditionRepeats() == 9, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
});
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -288,14 +287,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(null, "eventsCount"), null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"eventsCount\":" + eventsCount + "}");
for (int i = 0; i < eventsCount; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -319,13 +318,13 @@ public class AlarmRulesTest extends AbstractControllerTest {
);
Condition clearRule = new Condition("return powerConsumption < 3000;", null, clearDurationMs);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 5 seconds",
arguments, createRules, clearRule);
postTelemetry(deviceId, "{\"powerConsumption\":3500}");
Thread.sleep(createDurationMs - 2000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -334,9 +333,9 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"powerConsumption\":2000}");
Thread.sleep(clearDurationMs - 2000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
assertThat(alarmResult.getConditionDuration()).isBetween(clearDurationMs, clearDurationMs + 2000);
@ -363,12 +362,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<Long>(null, "duration"), null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds",
arguments, createRules, null);
postTelemetry(deviceId, "{\"powerConsumption\":3500}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"duration\":" + createDurationMs + "}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -391,10 +390,10 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<Long>(2000L, null), null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High power consumption during 2 seconds",
arguments, createRules, null);
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -424,12 +423,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
device.setCustomerId(customerId);
device = doPost("/api/device", device, Device.class);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"temperatureThreshold\":50}");
postTelemetry(deviceId, "{\"temperature\":51}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -462,21 +461,21 @@ public class AlarmRulesTest extends AbstractControllerTest {
"location", StringOperation.NOT_CONTAINS, new AlarmConditionValue<>(null, "locationFilter")
), null, null);
CalculatedField calculatedField = createAlarmCf(customerId, "New resident",
AlarmRuleDefinition alarmRule = createAlarmRule(customerId, "New resident",
arguments, createRules, clearRule);
loginSysAdmin();
postAttributes(tenantId, AttributeScope.SERVER_SCOPE, "{\"locationFilter\":\"Kyiv\"}");
loginTenantAdmin();
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Kyiv\"}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postAttributes(customerId, AttributeScope.SERVER_SCOPE, "{\"location\":\"Ukraine, Lviv\"}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.INDETERMINATE);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
@ -501,7 +500,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(null, "schedule"))
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
String schedule = """
{"timezone":"Europe/Kiev","items":[{"enabled":false,"dayOfWeek":1,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":2,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":3,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":4,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":5,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":6,"startsOn":0,"endsOn":0},{"enabled":false,"dayOfWeek":7,"startsOn":0,"endsOn":0}]}
@ -510,14 +509,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
schedule = schedule.replace("\"enabled\":false", "\"enabled\":true");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}");
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
// checking multiple debug events due to scheduled reevaluation (which also produces debug events)
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
@ -565,18 +564,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "No Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -596,19 +595,19 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
calculatedField.setName("New alarm type");
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule.setName("New alarm type");
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -628,18 +627,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration();
((TbelAlarmConditionExpression) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition().getExpression())
.setExpression("return temperature >= 50;");
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -662,13 +661,13 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
for (int i = 0; i < eventsCountMajor; i++) {
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(10);
}
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -676,18 +675,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
});
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR);
assertThat(alarmResult.getConditionRepeats()).isEqualTo(6);
});
// decreasing required events count for critical rule
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration();
((RepeatingAlarmCondition) configuration.getCreateRules().get(AlarmSeverity.CRITICAL).getCondition())
.setCount(new AlarmConditionValue<>(6, null));
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -719,20 +718,20 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= temperatureThreshold;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Thread.sleep(1000);
// not created because tenant's threshold 100 is used
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getArguments().get("temperatureThreshold")
((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getArguments().get("temperatureThreshold")
.setRefDynamicSourceConfiguration(null);
// using threshold 50 on device level
calculatedField = saveCalculatedField(calculatedField);
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -755,7 +754,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
Map<AlarmSeverity, Condition> createRules = Map.of(
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature and Humidity Alarm",
arguments, createRules, null, configuration -> {
configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"temperature is ${temperature}, humidity is ${humidity}"
@ -765,18 +764,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
postTelemetry(deviceId, "{\"temperature\":50}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("temperature is 50, humidity is 50");
});
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
((AlarmCalculatedFieldConfiguration) alarmRule.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"UPDATED temperature is ${temperature}, humidity is ${humidity}"
);
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
alarmRule = saveAlarmRule(alarmRule);
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isFalse();
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
@ -805,16 +804,16 @@ public class AlarmRulesTest extends AbstractControllerTest {
new AlarmConditionValue<>(schedule, null))
);
CalculatedField calculatedField = createAlarmCf(deviceId, "Illegal parking alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "Illegal parking alarm",
arguments, createRules, null);
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"parkingSpotOccupied\":true}");
Thread.sleep(10000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
assertThat(getLatestAlarmResult(alarmRule.getId())).isNull();
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
CalculatedFieldDebugEvent debugEvent = getDebugEvents(alarmRule.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
@ -838,11 +837,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null)
);
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm",
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm",
arguments, createRules, null);
postTelemetry(deviceId, "{\"temperature\":50}");
Alarm alarm = checkAlarmResult(calculatedField, alarmResult -> {
Alarm alarm = checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -851,7 +850,7 @@ public class AlarmRulesTest extends AbstractControllerTest {
doPost("/api/alarm/" + alarm.getId() + "/clear", AlarmInfo.class);
Thread.sleep(1000);
postTelemetry(deviceId, "{\"temperature\":50}");
checkAlarmResult(calculatedField, alarmResult -> {
checkAlarmResult(alarmRule, alarmResult -> {
assertThat(alarmResult.getAlarm().getId()).isNotEqualTo(alarm.getId());
assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
@ -861,21 +860,21 @@ public class AlarmRulesTest extends AbstractControllerTest {
// TODO: MSA tests
private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, Consumer<TbAlarmResult> assertion) {
return checkAlarmResult(calculatedField, null, assertion);
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer<TbAlarmResult> assertion) {
return checkAlarmResult(alarmRule, null, assertion);
}
private TbAlarmResult checkAlarmResult(CalculatedField calculatedField,
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule,
Predicate<TbAlarmResult> waitFor,
Consumer<TbAlarmResult> assertion) {
TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getLatestAlarmResult(calculatedField.getId()), result ->
.until(() -> getLatestAlarmResult(alarmRule.getId()), result ->
result != null && (waitFor == null || waitFor.test(result)));
assertion.accept(alarmResult);
Alarm alarm = alarmResult.getAlarm();
assertThat(alarm.getOriginator()).isEqualTo(originatorId);
assertThat(alarm.getType()).isEqualTo(calculatedField.getName());
assertThat(alarm.getType()).isEqualTo(alarmRule.getName());
return alarmResult;
}
@ -895,38 +894,37 @@ public class AlarmRulesTest extends AbstractControllerTest {
return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
}
private CalculatedField createAlarmCf(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
private AlarmRuleDefinition createAlarmRule(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>();
createConditions.forEach((severity, condition) -> {
createRules.put(severity, toAlarmRule(condition));
});
AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null;
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType);
calculatedField.setType(CalculatedFieldType.ALARM);
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition();
alarmRule.setEntityId(entityId);
alarmRule.setName(alarmType);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
configuration.setArguments(arguments);
configuration.setCreateRules(createRules);
configuration.setClearRule(clearRule);
calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all());
alarmRule.setConfiguration(configuration);
alarmRule.setDebugSettings(DebugSettings.all());
if (modifier.length > 0) {
modifier[0].accept(configuration);
}
CalculatedField savedCalculatedField = saveCalculatedField(calculatedField);
AlarmRuleDefinition savedAlarmRule = saveAlarmRule(alarmRule);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDebugEvents(savedCalculatedField.getId(), 1),
.until(() -> getDebugEvents(savedAlarmRule.getId(), 1),
events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return savedCalculatedField;
return savedAlarmRule;
}
private AlarmRule toAlarmRule(Condition condition) {

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

@ -94,6 +94,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldInfo;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -1482,6 +1483,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) {
return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class);
}
protected PageData<CalculatedField> getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" +
(type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink);

47
application/src/test/java/org/thingsboard/server/controller/AlarmCommentControllerTest.java

@ -44,7 +44,9 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -226,6 +228,18 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
doDelete("/api/alarm/" + alarm.getId() + "/comment/" + alarmComment.getId())
.andExpect(status().isOk());
Optional<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
assertThat(systemCommentOpt).isPresent();
AlarmCommentInfo systemComment = systemCommentOpt.get();
assertThat(systemComment.getId()).isEqualTo(alarmComment.getId());
assertThat(systemComment.getType()).isEqualTo(AlarmCommentType.SYSTEM);
assertThat(systemComment.getComment().get("text").asText()).isEqualTo(String.format("User %s deleted his comment",
TENANT_ADMIN_EMAIL));
AlarmComment expectedAlarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
@ -361,6 +375,39 @@ public class AlarmCommentControllerTest extends AbstractControllerTest {
Assert.assertTrue("Created alarm doesn't match the found one!", equals);
}
@Test
public void testShouldNotCreateOrUpdateSystemAlarmComment() throws Exception {
loginTenantAdmin();
AlarmComment alarmComment = AlarmComment.builder()
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().set("text", new TextNode("Acknowledged by tenant admin")))
.build();
AlarmComment created = doPost("/api/alarm/" + alarm.getId() + "/comment", alarmComment, AlarmComment.class);
assertThat(created.getType()).isEqualTo(AlarmCommentType.OTHER);
// acknowledge alarm to create system comment
doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk());
Optional<AlarmCommentInfo> systemCommentOpt = doGetTyped(
"/api/alarm/" + alarm.getId() + "/comment" + "?page=0&pageSize=10", new TypeReference<PageData<AlarmCommentInfo>>() {
}
).getData().stream().filter(alarmCommentInfo -> alarmCommentInfo.getType().equals(AlarmCommentType.SYSTEM)).findFirst();
assertThat(systemCommentOpt).isPresent();
AlarmCommentInfo systemComment = systemCommentOpt.get();
// system comment can't be updated with other type
systemComment.setType(AlarmCommentType.OTHER);
doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("System alarm comment can't be updated!")));
// system comment can't be updated with other text
systemComment.setType(AlarmCommentType.SYSTEM);
systemComment.setComment(JacksonUtil.newObjectNode().set("text", new TextNode("New system comment")));
doPost("/api/alarm/" + alarm.getId() + "/comment", systemComment).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("System alarm comment can't be updated!")));
}
private AlarmComment createAlarmComment(AlarmId alarmId, String text) {
AlarmComment alarmComment = AlarmComment.builder()
.comment(JacksonUtil.newObjectNode().set("text", new TextNode(text)))

420
application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java

@ -0,0 +1,420 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
import org.thingsboard.server.common.data.cf.AlarmRuleDefinitionInfo;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest
public class AlarmRuleControllerTest extends AbstractControllerTest {
private Tenant savedTenant;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = saveTenant(tenant);
assertThat(savedTenant).isNotNull();
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
deleteTenant(savedTenant.getId());
}
@Test
public void testSaveAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "High Temperature");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCreatedTime()).isGreaterThan(0);
assertThat(saved.getTenantId()).isEqualTo(savedTenant.getId());
assertThat(saved.getEntityId()).isEqualTo(testDevice.getId());
assertThat(saved.getName()).isEqualTo("High Temperature");
assertThat(saved.getConfiguration()).isNotNull();
assertThat(saved.getConfiguration().getCreateRules()).containsKey(AlarmSeverity.CRITICAL);
saved.setName("Updated Alarm Rule");
AlarmRuleDefinition updated = saveAlarmRule(saved);
assertThat(updated.getName()).isEqualTo("Updated Alarm Rule");
assertThat(updated.getVersion()).isEqualTo(saved.getVersion() + 1);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition alarmRule = createTestAlarmRule(testDevice.getId(), "Test Alarm");
AlarmRuleDefinition saved = saveAlarmRule(alarmRule);
AlarmRuleDefinition fetched = doGet("/api/alarm/rule/" + saved.getId().getId(), AlarmRuleDefinition.class);
assertThat(fetched).isNotNull();
assertThat(fetched).isEqualTo(saved);
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRuleById_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testGetAlarmRuleById_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doGet("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetAlarmRulesByEntityId() throws Exception {
Device device1 = createDevice("Device 1", "1234567890");
Device device2 = createDevice("Device 2", "0987654321");
AlarmRuleDefinition rule1 = saveAlarmRule(createTestAlarmRule(device1.getId(), "Rule 1"));
saveAlarmRule(createTestAlarmRule(device2.getId(), "Rule 2"));
PageData<AlarmRuleDefinition> result = doGetTypedWithPageLink(
"/api/alarm/rules/" + EntityType.DEVICE + "/" + device1.getUuidId() + "?",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getId()).isEqualTo(rule1.getId());
assertThat(result.getData().get(0).getName()).isEqualTo("Rule 1");
}
@Test
public void testGetAlarmRules() throws Exception {
Device device = createDevice("Device A", "1234567890");
AlarmRuleDefinition deviceRule = saveAlarmRule(createTestAlarmRule(device.getId(), "Device Alarm"));
DeviceProfile profile = doPost("/api/deviceProfile", createDeviceProfile("Profile A"), DeviceProfile.class);
AlarmRuleDefinition profileRule = saveAlarmRule(createTestAlarmRule(profile.getId(), "Profile Alarm"));
// All alarm rules
List<AlarmRuleDefinitionInfo> all = getAlarmRules(null, null);
assertThat(all).extracting(AlarmRuleDefinition::getName)
.contains("Device Alarm", "Profile Alarm");
// Filter by entity type: DEVICE
List<AlarmRuleDefinitionInfo> deviceRules = getAlarmRules(EntityType.DEVICE, null);
assertThat(deviceRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Filter by entity type: DEVICE_PROFILE
List<AlarmRuleDefinitionInfo> profileRules = getAlarmRules(EntityType.DEVICE_PROFILE, null);
assertThat(profileRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Profile Alarm");
// Filter by specific entity IDs
List<AlarmRuleDefinitionInfo> specificRules = getAlarmRules(EntityType.DEVICE, List.of(device.getUuidId()));
assertThat(specificRules).extracting(AlarmRuleDefinition::getName)
.containsOnly("Device Alarm");
// Verify entity names are populated
AlarmRuleDefinitionInfo deviceInfo = all.stream()
.filter(r -> r.getName().equals("Device Alarm")).findFirst().orElseThrow();
assertThat(deviceInfo.getEntityName()).isEqualTo("Device A");
AlarmRuleDefinitionInfo profileInfo = all.stream()
.filter(r -> r.getName().equals("Profile Alarm")).findFirst().orElseThrow();
assertThat(profileInfo.getEntityName()).isEqualTo("Profile A");
}
@Test
public void testGetAlarmRules_textSearch() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Temperature Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Humidity Alarm"));
PageData<AlarmRuleDefinitionInfo> result = doGetTypedWithPageLink(
"/api/alarm/rules?textSearch=Temp&",
new TypeReference<>() {}, new PageLink(10));
assertThat(result.getData()).hasSize(1);
assertThat(result.getData().get(0).getName()).isEqualTo("Temperature Alarm");
}
@Test
public void testGetAlarmRuleNames() throws Exception {
Device device = createDevice("Device A", "1234567890");
saveAlarmRule(createTestAlarmRule(device.getId(), "Alpha Alarm"));
saveAlarmRule(createTestAlarmRule(device.getId(), "Beta Alarm"));
PageData<String> names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(2);
assertThat(names.getData()).isSortedAccordingTo(Comparator.naturalOrder());
assertThat(names.getData()).contains("Alpha Alarm", "Beta Alarm");
names = getAlarmRuleNames(new PageLink(10, 0,
null, new SortOrder("", SortOrder.Direction.DESC)));
assertThat(names.getData()).isSortedAccordingTo(Comparator.reverseOrder());
names = getAlarmRuleNames(new PageLink(10, 0,
"Alpha", new SortOrder("", SortOrder.Direction.ASC)));
assertThat(names.getTotalElements()).isEqualTo(1);
assertThat(names.getData()).containsOnly("Alpha Alarm");
}
@Test
public void testDeleteAlarmRule() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "To Delete"));
assertThat(saved).isNotNull();
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
doGet("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_notFound() throws Exception {
doDelete("/api/alarm/rule/" + UUID.randomUUID())
.andExpect(status().isNotFound());
}
@Test
public void testDeleteAlarmRule_calculatedFieldNotAlarm() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
CalculatedField cf = createSimpleCalculatedField(testDevice.getId());
CalculatedField savedCf = doPost("/api/calculatedField", cf, CalculatedField.class);
doDelete("/api/alarm/rule/" + savedCf.getId().getId())
.andExpect(status().isNotFound());
doDelete("/api/calculatedField/" + savedCf.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
AlarmRuleDefinition saved = saveAlarmRule(createTestAlarmRule(testDevice.getId(), "Debug Test"));
doGet("/api/alarm/rule/" + saved.getId().getId() + "/debug")
.andExpect(status().isOk());
doDelete("/api/alarm/rule/" + saved.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testGetLatestAlarmRuleDebugEvent_notFound() throws Exception {
doGet("/api/alarm/rule/" + UUID.randomUUID() + "/debug")
.andExpect(status().isNotFound());
}
@Test
public void testTestAlarmRuleScript() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 55 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.has("output")).isTrue();
assertThat(result.has("error")).isTrue();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("true");
}
@Test
public void testTestAlarmRuleScript_returnsFalse() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "return temperature > 50;",
"arguments": {
"temperature": { "type": "SINGLE_VALUE", "ts": 1739776478057, "value": 30 }
}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isEmpty();
assertThat(result.get("output").asText()).isEqualTo("false");
}
@Test
public void testTestAlarmRuleScript_missingExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"arguments": {}
}
""");
doPost("/api/alarm/rule/testScript", request)
.andExpect(status().isBadRequest());
}
@Test
public void testTestAlarmRuleScript_invalidExpression() throws Exception {
JsonNode request = JacksonUtil.toJsonNode("""
{
"expression": "invalid syntax {{{{",
"arguments": {}
}
""");
JsonNode result = doPost("/api/alarm/rule/testScript", request, JsonNode.class);
assertThat(result).isNotNull();
assertThat(result.get("error").asText()).isNotEmpty();
}
// --- Helper methods ---
private AlarmRuleDefinition createTestAlarmRule(EntityId entityId, String name) {
AlarmRuleDefinition alarmRule = new AlarmRuleDefinition();
alarmRule.setEntityId(entityId);
alarmRule.setName(name);
alarmRule.setConfigurationVersion(1);
alarmRule.setAdditionalInfo(JacksonUtil.newObjectNode());
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
argument.setDefaultValue("0");
configuration.setArguments(Map.of("temperature", argument));
AlarmRule rule = new AlarmRule();
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
expression.setExpression("return temperature >= 50;");
SimpleAlarmCondition condition = new SimpleAlarmCondition();
condition.setExpression(expression);
rule.setCondition(condition);
configuration.setCreateRules(Map.of(AlarmSeverity.CRITICAL, rule));
alarmRule.setConfiguration(configuration);
return alarmRule;
}
private CalculatedField createSimpleCalculatedField(EntityId entityId) {
CalculatedField cf = new CalculatedField();
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("Simple CF");
cf.setConfigurationVersion(1);
cf.setAdditionalInfo(JacksonUtil.newObjectNode());
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument arg = new Argument();
arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
config.setArguments(Map.of("T", arg));
config.setExpression("T * 2");
TimeSeriesOutput output = new TimeSeriesOutput();
output.setName("result");
config.setOutput(output);
cf.setConfiguration(config);
return cf;
}
private List<AlarmRuleDefinitionInfo> getAlarmRules(EntityType entityType, List<UUID> entities) throws Exception {
StringBuilder url = new StringBuilder("/api/alarm/rules?");
if (entityType != null) {
url.append("entityType=").append(entityType).append("&");
}
if (entities != null) {
url.append("entities=").append(String.join(",",
entities.stream().map(UUID::toString).toList())).append("&");
}
return doGetTypedWithPageLink(url.toString(),
new TypeReference<PageData<AlarmRuleDefinitionInfo>>() {}, new PageLink(10)).getData();
}
private PageData<String> getAlarmRuleNames(PageLink pageLink) throws Exception {
return doGetTypedWithPageLink("/api/alarm/rules/names?",
new TypeReference<PageData<String>>() {}, pageLink);
}
}

73
application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java

@ -27,12 +27,19 @@ import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.adapter.NativeWebSocketSession;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.WebSocketSessionType;
@ -193,6 +200,72 @@ class TbWebSocketHandlerTest {
assertThat(msgs).map(Integer::parseInt).doesNotHaveDuplicates().hasSize(100);
}
// Regression test for the bug where publicUserSessionsMap was keyed by UserId(NULL_UUID),
// making maxWsSessionsPerPublicUser a global limit shared across all tenants.
// The limit is now scoped per-tenant.
@Test
void checkLimits_publicUserSessions_limitIsPerTenantNotGlobal() throws Exception {
TbTenantProfileCache tenantProfileCache = mock(TbTenantProfileCache.class);
ReflectionTestUtils.setField(wsHandler, "tenantProfileCache", tenantProfileCache);
int maxPublicSessions = 2;
TenantId tenant1 = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile1 = new TenantProfile();
profile1.createDefaultTenantProfileData();
profile1.getDefaultProfileConfiguration().setMaxWsSessionsPerPublicUser(maxPublicSessions);
willReturn(profile1).given(tenantProfileCache).get(tenant1);
TenantId tenant2 = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile2 = new TenantProfile();
profile2.createDefaultTenantProfileData();
profile2.getDefaultProfileConfiguration().setMaxWsSessionsPerPublicUser(maxPublicSessions);
willReturn(profile2).given(tenantProfileCache).get(tenant2);
Method checkLimits = TbWebSocketHandler.class.getDeclaredMethod(
"checkLimits", WebSocketSession.class, WebSocketSessionRef.class);
checkLimits.setAccessible(true);
// tenant1 fills up its limit
for (int i = 0; i < maxPublicSessions; i++) {
assertThat((boolean) checkLimits.invoke(wsHandler, mockWsSession("t1-" + i), mockPublicSessionRef(tenant1))).isTrue();
}
// tenant2 must get its own independent quota — this was the bug: with NULL_UUID as key
// all tenants shared one global counter, so tenant2 would be blocked here
for (int i = 0; i < maxPublicSessions; i++) {
assertThat((boolean) checkLimits.invoke(wsHandler, mockWsSession("t2-" + i), mockPublicSessionRef(tenant2)))
.as("tenant2 session %d should not be affected by tenant1's sessions", i + 1)
.isTrue();
}
// tenant1's (maxPublicSessions + 1)-th session must be rejected
NativeWebSocketSession overLimit = mockWsSession("t1-over");
assertThat((boolean) checkLimits.invoke(wsHandler, overLimit, mockPublicSessionRef(tenant1))).isFalse();
verify(overLimit).close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
}
private NativeWebSocketSession mockWsSession(String id) {
NativeWebSocketSession s = mock(NativeWebSocketSession.class);
willReturn(id).given(s).getId();
return s;
}
private WebSocketSessionRef mockPublicSessionRef(TenantId tenantId) {
CustomerId customerId = new CustomerId(UUID.randomUUID());
SecurityUser securityUser = mock(SecurityUser.class);
willReturn(tenantId).given(securityUser).getTenantId();
willReturn(customerId).given(securityUser).getCustomerId();
willReturn(new UserId(EntityId.NULL_UUID)).given(securityUser).getId();
willReturn(true).given(securityUser).isCustomerUser();
willReturn(new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, customerId.toString())).given(securityUser).getUserPrincipal();
WebSocketSessionRef ref = mock(WebSocketSessionRef.class);
willReturn(securityUser).given(ref).getSecurityCtx();
willReturn(UUID.randomUUID().toString()).given(ref).getSessionId();
return ref;
}
private AuthTestFixture createAuthTestFixture() throws IOException {
TbWebSocketHandler handler = spy(new TbWebSocketHandler());
willDoNothing().given(handler).close(any(), any());

517
application/src/test/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCacheTest.java

@ -0,0 +1,517 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
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.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultCalculatedFieldCacheTest {
@Mock
private CalculatedFieldService calculatedFieldService;
@Mock
private TbAssetProfileCache assetProfileCache;
@Mock
private TbDeviceProfileCache deviceProfileCache;
@Mock
private TbTenantProfileCache tenantProfileCache;
@Mock
private DeviceService deviceService;
@Mock
private AssetService assetService;
@Mock
private CustomerService customerService;
private DefaultCalculatedFieldCache cache;
@BeforeEach
public void setUp() {
// ActorSystemContext is only used in getCalculatedFieldCtx (not tested here), so null is safe
OwnerService ownerService = new OwnerService(deviceService, assetService, customerService);
cache = new DefaultCalculatedFieldCache(calculatedFieldService, assetProfileCache,
deviceProfileCache, tenantProfileCache, null, ownerService);
}
// --- Tenant deletion tests ---
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAllTenantCfsFromAllMaps() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceId device1 = new DeviceId(UUID.randomUUID());
DeviceId device2 = new DeviceId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant1, device1);
CalculatedField cf2 = addCfToCache(tenant2, device2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, tenant);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After eviction, getDynamicEntities triggers a fresh load from ownerService (empty)
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId cfEntity = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, cfEntity, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
assertThat(cache.getCalculatedField(cf.getId())).isNull();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
}
// --- Device/Asset deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsCfsForThatDevice() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, device, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceDeleted_evictsDeviceFromOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetId asset = new AssetId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, asset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, asset, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceCreated_addsDeviceToOwnerEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.CREATED));
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
}
// --- Customer deletion tests ---
@Test
public void onComponentLifecycleEvent_customerDeleted_evictsCustomerOwnerEntries() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
cache.addOwnerEntity(tenant, device);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
// The customer's owned-entities entry is evicted; fresh load returns empty
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- DeviceProfile/AssetProfile deletion tests ---
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId linkedDevice = new DeviceId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedDevice);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_deviceProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profile1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profile2 = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_deviceProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_evictsCfsForThatProfile() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_removesLinksForLinkedEntities() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId linkedAsset = new AssetId(UUID.randomUUID());
addCfToCache(tenant, profileId, linkedAsset);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedAsset)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_assetProfileDeleted_doesNotEvictOtherProfilesCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profile1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profile2 = new AssetProfileId(UUID.randomUUID());
CalculatedField cf1 = addCfToCache(tenant, profile1);
CalculatedField cf2 = addCfToCache(tenant, profile2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf1.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty();
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2);
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2);
}
@Test
public void onComponentLifecycleEvent_assetProfileUpdated_doesNotEvictCfs() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf);
}
// --- CalculatedField lifecycle tests ---
@Test
public void onComponentLifecycleEvent_calculatedFieldCreated_addsCfToCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenant, device, simpleCfConfig());
when(calculatedFieldService.findById(tenant, cfId)).thenReturn(cf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cfId, ComponentLifecycleEvent.CREATED));
assertThat(cache.getCalculatedField(cfId)).isEqualTo(cf);
assertThat(cache.getCalculatedFieldsByEntityId(device)).containsExactly(cf);
}
@Test
public void onComponentLifecycleEvent_calculatedFieldDeleted_evictsCfFromCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.DELETED));
assertThat(cache.getCalculatedField(cf.getId())).isNull();
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty();
}
@Test
public void onComponentLifecycleEvent_calculatedFieldUpdated_refreshesCfInCache() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
CalculatedField cf = addCfToCache(tenant, device);
CalculatedField updatedCf = buildCalculatedField(cf.getId(), tenant, device, simpleCfConfig());
updatedCf.setName("updated-name");
when(calculatedFieldService.findById(tenant, cf.getId())).thenReturn(updatedCf);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.UPDATED));
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf);
}
// --- evictOwner recursive traversal tests ---
@Test
public void evictOwner_customerDeleted_recursivelyEvictsDevicesOwnedByThatCustomer() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
// tenant owns customer (getOwner for CUSTOMER returns tenantId)
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the customer evicts the customer key and recursively cleans its owned set
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED));
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
@Test
public void evictOwner_tenantDeleted_recursivelyEvictsCustomerAndItsOwnedDevices() {
TenantId tenant = new TenantId(UUID.randomUUID());
CustomerId customer = new CustomerId(UUID.randomUUID());
DeviceId device = new DeviceId(UUID.randomUUID());
stubDeviceOwner(tenant, device, customer);
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device);
// deleting the tenant: evictOwner(tenant) finds customer (CUSTOMER type) and recurses into it
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// both levels must be gone
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(customer);
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device);
}
// --- TenantProfile lifecycle tests ---
@Test
public void onComponentLifecycleEvent_tenantProfileUpdated_callsHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED));
verify(spyCache).handleTenantProfileUpdate(profileId);
}
@Test
public void onComponentLifecycleEvent_tenantProfileDeleted_doesNotCallHandleTenantProfileUpdate() {
TenantId tenant = new TenantId(UUID.randomUUID());
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID());
DefaultCalculatedFieldCache spyCache = spy(cache);
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED));
verify(spyCache, never()).handleTenantProfileUpdate(any());
}
// --- Helpers ---
private void stubDeviceOwner(TenantId tenantId, DeviceId deviceId, EntityId ownerId) {
Device device = new Device();
device.setId(deviceId);
device.setTenantId(tenantId);
if (ownerId instanceof CustomerId customerId) {
device.setCustomerId(customerId);
}
// If ownerId is a TenantId, leaving customerId null means getOwnerId() returns tenantId
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
// Stubs for getOwnedEntities iteration (empty pages — device is added explicitly)
when(deviceService.findDeviceInfosByFilter(any(), any())).thenReturn(PageData.emptyPageData());
when(assetService.findAssetsByTenantIdAndCustomerId(any(), any(), any())).thenReturn(PageData.emptyPageData());
if (ownerId instanceof TenantId) {
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData());
}
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig());
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId, EntityId linkedEntity) {
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID());
CalculatedFieldConfiguration config = linkedEntityCfConfig(tenantId, cfId, linkedEntity);
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, config);
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf);
cache.addCalculatedField(tenantId, cfId);
return cf;
}
private CalculatedField buildCalculatedField(CalculatedFieldId id, TenantId tenantId, EntityId entityId, CalculatedFieldConfiguration config) {
CalculatedField cf = new CalculatedField();
cf.setId(id);
cf.setTenantId(tenantId);
cf.setEntityId(entityId);
cf.setType(CalculatedFieldType.SIMPLE);
cf.setName("test-cf-" + id.getId());
cf.setConfiguration(config);
return cf;
}
private CalculatedFieldConfiguration simpleCfConfig() {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
when(config.getReferencedEntities()).thenReturn(Collections.emptySet());
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList());
return config;
}
private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) {
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class);
CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId);
when(config.getReferencedEntities()).thenReturn(Set.of(linkedEntity));
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link));
when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link);
return config;
}
}

159
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCacheTest.java

@ -0,0 +1,159 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbAssetProfileCacheTest {
@Mock
private AssetProfileService assetProfileService;
@Mock
private AssetService assetService;
private DefaultTbAssetProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbAssetProfileCache(assetProfileService, assetService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// After deletion tenant1 profile should be reloaded from service on next get
when(assetProfileService.findAssetProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
AssetId assetId = new AssetId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadAssetMappingIntoCache(tenant, assetId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, asset-to-profile mapping should be gone; get() should try to reload
when(assetService.findAssetById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, assetId)).isNull();
verify(assetService, times(2)).findAssetById(tenant, assetId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new AssetId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(assetProfileService, times(1)).findAssetProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID());
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID());
AssetProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(assetProfileService, times(1)).findAssetProfileById(tenant1, profileId1);
}
// --- Helpers ---
private AssetProfile loadProfileIntoCache(TenantId tenantId, AssetProfileId profileId) {
AssetProfile profile = new AssetProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(assetProfileService.findAssetProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadAssetMappingIntoCache(TenantId tenantId, AssetId assetId, AssetProfileId profileId) {
Asset asset = new Asset();
asset.setId(assetId);
asset.setAssetProfileId(profileId);
when(assetService.findAssetById(tenantId, assetId)).thenReturn(asset);
cache.get(tenantId, assetId);
}
}

160
application/src/test/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCacheTest.java

@ -0,0 +1,160 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class DefaultTbDeviceProfileCacheTest {
@Mock
private DeviceProfileService deviceProfileService;
@Mock
private DeviceService deviceService;
private DefaultTbDeviceProfileCache cache;
@BeforeEach
public void setUp() {
cache = new DefaultTbDeviceProfileCache(deviceProfileService, deviceService);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceProfilesForThatTenant() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED));
// After deletion tenant1 profile should be reloaded from service on next get
when(deviceProfileService.findDeviceProfileById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant1, profileId1)).isNull();
// tenant2 profile should still be served from cache (no extra service call)
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant2, profileId2);
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceMappingsForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
DeviceId deviceId = new DeviceId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
loadDeviceMappingIntoCache(tenant, deviceId, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// After tenant deletion, device-to-profile mapping should be gone; get() should try to reload
when(deviceService.findDeviceById(any(), any())).thenReturn(null);
assertThat(cache.get(tenant, deviceId)).isNull();
verify(deviceService, times(2)).findDeviceById(tenant, deviceId); // once on load, once after eviction
}
@Test
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() {
TenantId tenant = new TenantId(UUID.randomUUID());
EntityId listenerId = new DeviceId(UUID.randomUUID());
AtomicInteger callCount = new AtomicInteger();
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED));
// Evicting a profile after tenant deletion should not trigger the removed listener
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.evict(tenant, profileId);
assertThat(callCount.get()).isZero();
}
@Test
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() {
TenantId tenant = new TenantId(UUID.randomUUID());
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID());
loadProfileIntoCache(tenant, profileId);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED));
// Profile should still be served from cache without hitting the service again
cache.get(tenant, profileId);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant, profileId);
}
@Test
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() {
TenantId tenant1 = new TenantId(UUID.randomUUID());
TenantId tenant2 = new TenantId(UUID.randomUUID());
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID());
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID());
DeviceProfile profile1 = loadProfileIntoCache(tenant1, profileId1);
loadProfileIntoCache(tenant2, profileId2);
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED));
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1);
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant1, profileId1);
}
// --- Helpers ---
private DeviceProfile loadProfileIntoCache(TenantId tenantId, DeviceProfileId profileId) {
DeviceProfile profile = new DeviceProfile();
profile.setId(profileId);
profile.setTenantId(tenantId);
when(deviceProfileService.findDeviceProfileById(tenantId, profileId)).thenReturn(profile);
cache.get(tenantId, profileId);
return profile;
}
private void loadDeviceMappingIntoCache(TenantId tenantId, DeviceId deviceId, DeviceProfileId profileId) {
Device device = new Device();
device.setId(deviceId);
device.setDeviceProfileId(profileId);
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device);
cache.get(tenantId, deviceId);
}
}

277
application/src/test/java/org/thingsboard/server/service/ws/DefaultWebSocketServiceTest.java

@ -0,0 +1,277 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ws;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.ws.notification.NotificationCommandsHandler;
import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
class DefaultWebSocketServiceTest {
DefaultWebSocketService service;
TbTenantProfileCache tenantProfileCache;
WebSocketMsgEndpoint msgEndpoint;
@BeforeEach
void setUp() {
tenantProfileCache = mock(TbTenantProfileCache.class);
msgEndpoint = mock(WebSocketMsgEndpoint.class);
service = new DefaultWebSocketService(
mock(TbLocalSubscriptionService.class),
mock(TbEntityDataSubscriptionService.class),
mock(NotificationCommandsHandler.class),
msgEndpoint,
mock(AccessValidator.class),
mock(AttributesService.class),
mock(TimeseriesService.class),
mock(TbServiceInfoProvider.class),
tenantProfileCache
);
}
// Regression test: publicUserSubscriptionsMap must be keyed by TenantId, not UserId(NULL_UUID).
// With the old UserId(NULL_UUID) key, all tenants shared one global subscription counter.
@Test
void processSubscription_publicUserSubscriptionsMap_isPerTenantNotGlobal() throws Exception {
int maxPublicSubscriptions = 2;
TenantId tenant1 = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile1 = new TenantProfile();
profile1.createDefaultTenantProfileData();
profile1.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile1).given(tenantProfileCache).get(tenant1);
TenantId tenant2 = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile2 = new TenantProfile();
profile2.createDefaultTenantProfileData();
profile2.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile2).given(tenantProfileCache).get(tenant2);
// tenant1 fills up its quota
for (int i = 0; i < maxPublicSubscriptions; i++) {
assertThat(service.processSubscription(mockPublicSessionRef(tenant1, "t1-session-" + i), subscriptionCmd(i)))
.as("tenant1 subscription %d should be accepted", i + 1)
.isTrue();
}
// tenant2 must have its own independent quota — this was the bug:
// with UserId(NULL_UUID) as key all tenants shared one counter, so tenant2 would be blocked here
for (int i = 0; i < maxPublicSubscriptions; i++) {
assertThat(service.processSubscription(mockPublicSessionRef(tenant2, "t2-session-" + i), subscriptionCmd(i)))
.as("tenant2 subscription %d should not be affected by tenant1's subscriptions", i + 1)
.isTrue();
}
// tenant1's (maxPublicSubscriptions + 1)-th subscription must be rejected
assertThat(service.processSubscription(mockPublicSessionRef(tenant1, "t1-session-over"), subscriptionCmd(99)))
.as("tenant1 should be rejected after exceeding its limit")
.isFalse();
// Verify that publicUserSubscriptionsMap has separate entries per tenant
@SuppressWarnings("unchecked")
ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
assertThat(publicUserSubscriptionsMap).as("map should contain tenant1").containsKey(tenant1);
assertThat(publicUserSubscriptionsMap).as("map should contain tenant2").containsKey(tenant2);
assertThat(publicUserSubscriptionsMap).as("map must not have a single NULL_UUID entry for all tenants")
.doesNotContainKey(new TenantId(EntityId.NULL_UUID));
assertThat(publicUserSubscriptionsMap.get(tenant1))
.as("tenant1 should have exactly %d subscriptions", maxPublicSubscriptions)
.hasSize(maxPublicSubscriptions);
assertThat(publicUserSubscriptionsMap.get(tenant2))
.as("tenant2 should have exactly %d subscriptions", maxPublicSubscriptions)
.hasSize(maxPublicSubscriptions);
}
@Test
void processSubscription_publicUserSubscriptionsMap_subscriptionIdFormat() {
int maxPublicSubscriptions = 5;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile = new TenantProfile();
profile.createDefaultTenantProfileData();
profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile).given(tenantProfileCache).get(tenantId);
String sessionId = "my-session-id";
int cmdId = 42;
WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
service.processSubscription(sessionRef, subscriptionCmd(cmdId));
@SuppressWarnings("unchecked")
ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
Set<String> subs = publicUserSubscriptionsMap.get(tenantId);
assertThat(subs).hasSize(1);
assertThat(subs.iterator().next()).isEqualTo("[" + sessionId + "]:[" + cmdId + "]");
}
@Test
void processSubscription_unsubscribe_removesEntryFromPublicUserSubscriptionsMap() {
int maxPublicSubscriptions = 5;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile = new TenantProfile();
profile.createDefaultTenantProfileData();
profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile).given(tenantProfileCache).get(tenantId);
String sessionId = "session-1";
int cmdId = 1;
WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
service.processSubscription(sessionRef, subscriptionCmd(cmdId));
@SuppressWarnings("unchecked")
ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(1);
AttributesSubscriptionCmd unsubCmd = subscriptionCmd(cmdId);
unsubCmd.setUnsubscribe(true);
service.processSubscription(sessionRef, unsubCmd);
assertThat(publicUserSubscriptionsMap.get(tenantId)).isEmpty();
}
@Test
void processSubscription_unsubscribe_freesSlotForNewSubscription() {
int maxPublicSubscriptions = 1;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile = new TenantProfile();
profile.createDefaultTenantProfileData();
profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile).given(tenantProfileCache).get(tenantId);
WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, "session-1");
service.processSubscription(sessionRef, subscriptionCmd(1));
// slot is full — second subscription on same session should be rejected
assertThat(service.processSubscription(sessionRef, subscriptionCmd(2))).isFalse();
// unsubscribe cmd 1 to free the slot
AttributesSubscriptionCmd unsubCmd = subscriptionCmd(1);
unsubCmd.setUnsubscribe(true);
service.processSubscription(sessionRef, unsubCmd);
// now a new subscription should succeed
assertThat(service.processSubscription(sessionRef, subscriptionCmd(3)))
.as("new subscription should succeed after unsubscribe freed the slot")
.isTrue();
}
@Test
void processSessionClose_removesAllSessionSubscriptionsFromPublicUserSubscriptionsMap() {
int maxPublicSubscriptions = 10;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile = new TenantProfile();
profile.createDefaultTenantProfileData();
profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile).given(tenantProfileCache).get(tenantId);
String sessionId = "closing-session";
WebSocketSessionRef sessionRef = mockPublicSessionRef(tenantId, sessionId);
service.processSubscription(sessionRef, subscriptionCmd(1));
service.processSubscription(sessionRef, subscriptionCmd(2));
service.processSubscription(sessionRef, subscriptionCmd(3));
@SuppressWarnings("unchecked")
ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(3);
service.processSessionClose(sessionRef);
assertThat(publicUserSubscriptionsMap.get(tenantId)).isEmpty();
}
@Test
void processSessionClose_onlyRemovesClosedSessionSubscriptions() {
int maxPublicSubscriptions = 10;
TenantId tenantId = TenantId.fromUUID(UUID.randomUUID());
TenantProfile profile = new TenantProfile();
profile.createDefaultTenantProfileData();
profile.getDefaultProfileConfiguration().setMaxWsSubscriptionsPerPublicUser(maxPublicSubscriptions);
willReturn(profile).given(tenantProfileCache).get(tenantId);
WebSocketSessionRef session1 = mockPublicSessionRef(tenantId, "session-1");
WebSocketSessionRef session2 = mockPublicSessionRef(tenantId, "session-2");
service.processSubscription(session1, subscriptionCmd(1));
service.processSubscription(session1, subscriptionCmd(2));
service.processSubscription(session2, subscriptionCmd(1));
@SuppressWarnings("unchecked")
ConcurrentMap<TenantId, Set<String>> publicUserSubscriptionsMap =
(ConcurrentMap<TenantId, Set<String>>) ReflectionTestUtils.getField(service, "publicUserSubscriptionsMap");
assertThat(publicUserSubscriptionsMap.get(tenantId)).hasSize(3);
service.processSessionClose(session1);
Set<String> remaining = publicUserSubscriptionsMap.get(tenantId);
assertThat(remaining).hasSize(1);
assertThat(remaining).allMatch(subId -> subId.startsWith("[session-2]"));
}
private WebSocketSessionRef mockPublicSessionRef(TenantId tenantId, String sessionId) {
CustomerId customerId = new CustomerId(UUID.randomUUID());
SecurityUser securityUser = mock(SecurityUser.class);
willReturn(tenantId).given(securityUser).getTenantId();
willReturn(customerId).given(securityUser).getCustomerId();
willReturn(new UserId(EntityId.NULL_UUID)).given(securityUser).getId();
willReturn(true).given(securityUser).isCustomerUser();
willReturn(new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, customerId.toString())).given(securityUser).getUserPrincipal();
WebSocketSessionRef ref = mock(WebSocketSessionRef.class);
willReturn(securityUser).given(ref).getSecurityCtx();
willReturn(sessionId).given(ref).getSessionId();
return ref;
}
private AttributesSubscriptionCmd subscriptionCmd(int cmdId) {
AttributesSubscriptionCmd cmd = new AttributesSubscriptionCmd();
cmd.setCmdId(cmdId);
return cmd;
}
}

245
application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.system.SystemPatchApplier;
@ -71,6 +72,9 @@ public class SystemPatchApplierTest {
@Mock
private InstallScripts installScripts;
@Mock
private DatabaseSchemaSettingsService schemaSettingsService;
@Mock
private WidgetTypeService widgetTypeService;
@ -373,6 +377,247 @@ public class SystemPatchApplierTest {
verify(widgetTypeService, times(1)).saveWidgetType(any());
}
// --- isVersionIncreased tests ---
@ParameterizedTest(name = "isVersionIncreased: {0} (package={1}, db={2}) -> {3}")
@MethodSource("provideVersionComparisonTestCases")
void testIsVersionIncreased(String testName, SystemPatchApplier.VersionInfo packageVersion,
SystemPatchApplier.VersionInfo dbVersion, boolean expected) {
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionIncreased", packageVersion, dbVersion);
assertEquals(expected, result, testName);
}
private static Stream<Arguments> provideVersionComparisonTestCases() {
return Stream.of(
// Maintenance digit increases within same LTS family
Arguments.of("maintenance increased",
new SystemPatchApplier.VersionInfo(4, 3, 1, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
Arguments.of("maintenance increased by more than one",
new SystemPatchApplier.VersionInfo(4, 3, 3, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
// Patch digit increases within same maintenance
Arguments.of("patch increased",
new SystemPatchApplier.VersionInfo(4, 3, 0, 1),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
Arguments.of("patch increased by more than one",
new SystemPatchApplier.VersionInfo(4, 3, 0, 5),
new SystemPatchApplier.VersionInfo(4, 3, 0, 2), true),
// Both maintenance and patch increased
Arguments.of("maintenance and patch both increased",
new SystemPatchApplier.VersionInfo(4, 3, 1, 1),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), true),
// Maintenance increased, patch value is lower (irrelevant — maintenance wins)
Arguments.of("maintenance increased, patch is lower",
new SystemPatchApplier.VersionInfo(4, 3, 2, 0),
new SystemPatchApplier.VersionInfo(4, 3, 1, 5), true),
// Same version — no increase
Arguments.of("same version",
new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
Arguments.of("same version with non-zero parts",
new SystemPatchApplier.VersionInfo(4, 3, 1, 2),
new SystemPatchApplier.VersionInfo(4, 3, 1, 2), false),
// Decreased versions — no increase
Arguments.of("maintenance decreased",
new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 1, 0), false),
Arguments.of("patch decreased",
new SystemPatchApplier.VersionInfo(4, 3, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 1), false),
// Different major — different family, skip
Arguments.of("different major",
new SystemPatchApplier.VersionInfo(5, 3, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
Arguments.of("major decreased",
new SystemPatchApplier.VersionInfo(3, 3, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
// Different minor — different LTS family, skip
Arguments.of("minor increased (different LTS family)",
new SystemPatchApplier.VersionInfo(4, 4, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false),
Arguments.of("minor decreased",
new SystemPatchApplier.VersionInfo(4, 2, 0, 0),
new SystemPatchApplier.VersionInfo(4, 3, 0, 0), false)
);
}
// --- isVersionChanged tests ---
@Test
void whenVersionIncreased_thenVersionChangedReturnsTrue() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
assertTrue(result);
}
@Test
void whenVersionNotIncreased_thenVersionChangedReturnsFalse() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
assertFalse(result);
}
@Test
void whenVersionUnparseable_thenVersionChangedReturnsFalse() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("invalid");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
assertFalse(result);
}
@Test
void whenDbVersionUnparseable_thenVersionChangedReturnsFalse() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("bad");
Boolean result = ReflectionTestUtils.invokeMethod(reconciler, "isVersionChanged");
assertFalse(result);
}
// --- updateLtsSqlSchema tests ---
@Test
void whenLtsSqlFileExists_thenExecutesSql() throws Exception {
Path dataDir = tempDir.resolve("data");
Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
Files.createDirectories(ltsDir);
Files.writeString(ltsDir.resolve("schema_update.sql"), "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);");
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
verify(jdbcTemplate).execute("ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);");
}
@Test
void whenLtsSqlFileDoesNotExist_thenSkips() {
Path dataDir = tempDir.resolve("data");
// Don't create the file
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
verify(jdbcTemplate, never()).execute(anyString());
}
@Test
void whenLtsSqlFileHasMultipleStatements_thenExecutesAll() throws Exception {
Path dataDir = tempDir.resolve("data");
Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
Files.createDirectories(ltsDir);
String sql = "DO $$ BEGIN\n" +
" IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'test_type') THEN\n" +
" CREATE TYPE test_type AS ENUM ('A', 'B');\n" +
" END IF;\n" +
"END $$;\n" +
"ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);";
Files.writeString(ltsDir.resolve("schema_update.sql"), sql);
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
ReflectionTestUtils.invokeMethod(reconciler, "updateLtsSqlSchema");
verify(jdbcTemplate).execute(sql);
}
// --- applyPatchIfNeeded flow tests ---
@Test
void whenVersionIncreased_thenAppliesLtsSqlBeforeViewsAndWidgets() throws Exception {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true);
when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true);
Path dataDir = tempDir.resolve("data");
Path ltsDir = dataDir.resolve("upgrade").resolve("lts");
Files.createDirectories(ltsDir);
Files.writeString(ltsDir.resolve("schema_update.sql"), "SELECT 1;");
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
Path widgetTypesDir = tempDir.resolve("widget_types");
Files.createDirectories(widgetTypesDir);
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir);
ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
// LTS SQL was executed
verify(jdbcTemplate).execute("SELECT 1;");
// Schema version was updated
verify(schemaSettingsService).updateSchemaVersion();
}
@Test
void whenVersionNotIncreased_thenSkipsEverything() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.0.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
// No lock acquired
verify(jdbcTemplate, never()).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong());
// No schema update
verify(schemaSettingsService, never()).updateSchemaVersion();
}
@Test
void whenLockNotAcquired_thenSkipsPatchApplication() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.1.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(false);
ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
verify(schemaSettingsService, never()).updateSchemaVersion();
verify(jdbcTemplate, never()).execute(anyString());
}
@Test
void whenMaintenanceVersionIncreased_thenAppliesPatch() throws Exception {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.3.2.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.1.0");
when(jdbcTemplate.queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong())).thenReturn(true);
when(jdbcTemplate.queryForObject(contains("pg_advisory_unlock"), eq(Boolean.class), anyLong())).thenReturn(true);
Path dataDir = tempDir.resolve("data");
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
Path widgetTypesDir = tempDir.resolve("widget_types");
Files.createDirectories(widgetTypesDir);
when(installScripts.getWidgetTypesDir()).thenReturn(widgetTypesDir);
ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
verify(schemaSettingsService).updateSchemaVersion();
}
@Test
void whenDifferentLtsFamily_thenSkipsPatch() {
when(schemaSettingsService.getPackageSchemaVersion()).thenReturn("4.4.0.0");
when(schemaSettingsService.getDbSchemaVersion()).thenReturn("4.3.0.0");
ReflectionTestUtils.invokeMethod(reconciler, "applyPatchIfNeeded");
verify(jdbcTemplate, never()).queryForObject(contains("pg_try_advisory_lock"), eq(Boolean.class), anyLong());
verify(schemaSettingsService, never()).updateSchemaVersion();
}
private static Stream<Arguments> provideDescriptorComparisonTestCases() {
return Stream.of(
Arguments.of("Both null", null, null, true),

11
common/cache/src/main/java/org/thingsboard/server/cache/TbJsonRedisSerializer.java

@ -18,6 +18,8 @@ package org.thingsboard.server.cache;
import org.springframework.data.redis.serializer.SerializationException;
import org.thingsboard.common.util.JacksonUtil;
import java.io.IOException;
public class TbJsonRedisSerializer<K, V> implements TbRedisSerializer<K, V> {
private final Class<V> clazz;
@ -33,6 +35,13 @@ public class TbJsonRedisSerializer<K, V> implements TbRedisSerializer<K, V> {
@Override
public V deserialize(K key, byte[] bytes) throws SerializationException {
return JacksonUtil.fromBytes(bytes, clazz);
if (bytes == null) {
return null;
}
try {
return JacksonUtil.IGNORE_UNKNOWN_PROPERTIES_JSON_MAPPER.readValue(bytes, clazz);
} catch (IOException e) {
throw new SerializationException("Failed to deserialize cached value", e);
}
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -134,7 +134,7 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getPhone();
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Email", example = "example@company.com")
@Schema(description = "Email", example = "example@company.com")
@Override
public String getEmail() {
return super.getEmail();

2
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -176,7 +176,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.label = label;
}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Device Profile Id.")
@Schema(description = "JSON object with Device Profile Id. If not provided, the type will be used to determine the profile. If neither deviceProfileId nor type is specified, the default device profile will be used.")
public DeviceProfileId getDeviceProfileId() {
return deviceProfileId;
}

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

@ -22,6 +22,7 @@ import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.Value;
import org.thingsboard.server.common.data.id.DashboardId;
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.TenantId;
@ -75,4 +76,10 @@ public class DeviceProfileInfo extends EntityInfo {
profile.getType(), profile.getTransportType());
}
@Override
@Schema(implementation = DeviceProfileId.class, description = "JSON object with the Device Profile Id.")
public EntityId getId() {
return super.getId();
}
}

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

@ -46,7 +46,6 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
private static final long serialVersionUID = 9017108678716011604L;
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY,
description = "JSON object representing the ID of the tenant associated with this AI model",
example = "e3c4b7d2-5678-4a9b-0c1d-2e3f4a5b6c7d"
@ -54,7 +53,6 @@ public final class AiModel extends BaseData<AiModelId> implements HasTenantId, H
private TenantId tenantId;
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_ONLY,
description = "Version of the AI model record; increments automatically whenever the record is changed",
example = "7",

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java

@ -39,6 +39,7 @@ public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationReques
private TenantId tenantId;
@Schema(description = "JSON object with Customer Id", accessMode = Schema.AccessMode.READ_ONLY)
private CustomerId customerId;
@NoXss
@NotNull
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "representing type of the Alarm", example = "High Temperature Alarm")
@Length(fieldName = "type")

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java

@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.id.DashboardId;
@Schema(name = "AlarmRuleDefinition")
@Schema
@Data
@AllArgsConstructor
@NoArgsConstructor

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java

@ -32,7 +32,6 @@ import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSch
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule;
@Schema(
name = "AlarmRuleCondition",
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "SIMPLE", schema = SimpleAlarmCondition.class),

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java

@ -25,7 +25,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.util.concurrent.TimeUnit;
@Schema(name = "AlarmRuleDurationCondition")
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java

@ -22,7 +22,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(name = "AlarmRuleRepeatingCondition")
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/SimpleAlarmCondition.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.alarm.rule.condition;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "AlarmRuleSimpleCondition")
@Schema
public class SimpleAlarmCondition extends AlarmCondition {
@Override

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java

@ -32,7 +32,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.List;
@Schema(name = "AlarmRuleConditionFilter")
@Schema
@Data
public class AlarmConditionFilter implements Serializable {

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java

@ -37,6 +37,7 @@ public class BooleanFilterPredicate implements SimpleKeyFilterPredicate<Boolean>
return FilterPredicateType.BOOLEAN;
}
@Schema(name = "AlarmRuleBooleanOperation")
public enum BooleanOperation {
EQUAL,
NOT_EQUAL

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java

@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionVal
import java.util.concurrent.TimeUnit;
@Schema(name = "AlarmRuleNoDataFilterPredicate")
@Schema
@Data
@AllArgsConstructor
@NoArgsConstructor

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

@ -41,6 +41,7 @@ public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double>
return FilterPredicateType.NUMERIC;
}
@Schema(name = "AlarmRuleNumericOperation")
public enum NumericOperation {
EQUAL,
NOT_EQUAL,

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java

@ -38,6 +38,7 @@ public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
return FilterPredicateType.STRING;
}
@Schema(name= "AlarmRuleStringOperation")
public enum StringOperation {
EQUAL,
NOT_EQUAL,

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmSchedule.java

@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
@Schema(
name = "AlarmRuleSchedule",
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "ANY_TIME", schema = AnyTimeSchedule.class),

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AlarmScheduleType.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.alarm.rule.condition.schedule;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "AlarmRuleScheduleType")
@Schema
public enum AlarmScheduleType {
ANY_TIME,
SPECIFIC_TIME,

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/AnyTimeSchedule.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.alarm.rule.condition.schedule;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "AlarmRuleAnyTimeSchedule")
@Schema
public class AnyTimeSchedule implements AlarmSchedule {
@Override

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeSchedule.java

@ -20,7 +20,7 @@ import lombok.Data;
import java.util.List;
@Schema(name = "AlarmRuleCustomTimeSchedule")
@Schema
@Data
public class CustomTimeSchedule implements AlarmSchedule {

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/CustomTimeScheduleItem.java

@ -21,7 +21,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
@Schema(name = "AlarmRuleCustomTimeScheduleItem")
@Schema
@Data
public class CustomTimeScheduleItem implements Serializable {

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/schedule/SpecificTimeSchedule.java

@ -20,7 +20,7 @@ import lombok.Data;
import java.util.Set;
@Schema(name = "AlarmRuleSpecificTimeSchedule")
@Schema
@Data
public class SpecificTimeSchedule implements AlarmSchedule {

7
common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java

@ -23,6 +23,7 @@ import lombok.ToString;
import lombok.Value;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -66,4 +67,10 @@ public class AssetProfileInfo extends EntityInfo {
this(profile.getId(), profile.getTenantId(), profile.getName(), profile.getImage(), profile.getDefaultDashboardId());
}
@Override
@Schema(implementation = AssetProfileId.class, description = "JSON object with the Asset Profile Id.")
public EntityId getId() {
return super.getId();
}
}

166
common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java

@ -0,0 +1,166 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasAdditionalInfo;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
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.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class AlarmRuleDefinition extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings, HasAdditionalInfo {
private TenantId tenantId;
private EntityId entityId;
@NoXss
@Length(fieldName = "name")
@Schema(description = "User defined name of the alarm rule.")
private String name;
@Deprecated
@Schema(description = "Enable/disable debug. ", example = "false", deprecated = true)
private boolean debugMode;
@Schema(description = "Debug settings object.")
private DebugSettings debugSettings;
@Schema(description = "Version of alarm rule configuration.", example = "0")
private int configurationVersion;
@Schema(implementation = AlarmCalculatedFieldConfiguration.class)
@Valid
@NotNull
private AlarmCalculatedFieldConfiguration configuration;
private Long version;
@NoXss
@Schema(description = "Additional parameters of the alarm rule. " +
"May include: 'description' (string).",
implementation = com.fasterxml.jackson.databind.JsonNode.class,
example = "{\"description\":\"High temperature alarm rule\"}")
private JsonNode additionalInfo;
public AlarmRuleDefinition() {}
public AlarmRuleDefinition(CalculatedFieldId id) {
super(id);
}
public AlarmRuleDefinition(AlarmRuleDefinition alarmRuleDefinition) {
super(alarmRuleDefinition);
this.tenantId = alarmRuleDefinition.tenantId;
this.entityId = alarmRuleDefinition.entityId;
this.name = alarmRuleDefinition.name;
this.debugMode = alarmRuleDefinition.debugMode;
this.debugSettings = alarmRuleDefinition.debugSettings;
this.configurationVersion = alarmRuleDefinition.configurationVersion;
this.configuration = alarmRuleDefinition.configuration;
this.version = alarmRuleDefinition.version;
this.additionalInfo = alarmRuleDefinition.additionalInfo;
}
@Schema(description = "JSON object with the Alarm Rule Id. Referencing non-existing Alarm Rule Id will cause error.")
@Override
public CalculatedFieldId getId() {
return super.getId();
}
@Schema(description = "Timestamp of the alarm rule creation, in milliseconds", example = "1609459200000", accessMode = Schema.AccessMode.READ_ONLY)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
// Getter is ignored for serialization
@JsonIgnore
public boolean isDebugMode() {
return debugMode;
}
// Setter is annotated for deserialization
@JsonSetter
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
public CalculatedField toCalculatedField() {
CalculatedField cf = new CalculatedField();
cf.setId(this.id);
cf.setCreatedTime(this.createdTime);
cf.setTenantId(this.tenantId);
cf.setEntityId(this.entityId);
cf.setType(CalculatedFieldType.ALARM);
cf.setName(this.name);
cf.setDebugMode(this.debugMode);
cf.setDebugSettings(this.debugSettings);
cf.setConfigurationVersion(this.configurationVersion);
cf.setConfiguration(this.configuration);
cf.setVersion(this.version);
cf.setAdditionalInfo(this.additionalInfo);
return cf;
}
public static AlarmRuleDefinition fromCalculatedField(CalculatedField cf) {
AlarmRuleDefinition def = new AlarmRuleDefinition();
def.setId(cf.getId());
def.setCreatedTime(cf.getCreatedTime());
def.setTenantId(cf.getTenantId());
def.setEntityId(cf.getEntityId());
def.setName(cf.getName());
def.setDebugMode(cf.isDebugMode());
def.setDebugSettings(cf.getDebugSettings());
def.setConfigurationVersion(cf.getConfigurationVersion());
if (!(cf.getConfiguration() instanceof AlarmCalculatedFieldConfiguration config)) {
throw new IllegalArgumentException("Expected ALARM calculated field, got " + cf.getType());
}
def.setConfiguration(config);
def.setVersion(cf.getVersion());
def.setAdditionalInfo(cf.getAdditionalInfo());
return def;
}
@Override
public String toString() {
return new StringBuilder()
.append("AlarmRuleDefinition[")
.append("tenantId=").append(tenantId)
.append(", entityId=").append(entityId)
.append(", name='").append(name)
.append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration)
.append(", additionalInfo=").append(additionalInfo)
.append(", version=").append(version)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
.toString();
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.cf;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class AlarmRuleDefinitionInfo extends AlarmRuleDefinition {
private String entityName;
public AlarmRuleDefinitionInfo(AlarmRuleDefinition alarmRuleDefinition, String entityName) {
super(alarmRuleDefinition);
this.entityName = entityName;
}
public static AlarmRuleDefinitionInfo fromCalculatedFieldInfo(CalculatedFieldInfo cfi) {
AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi);
return new AlarmRuleDefinitionInfo(def, cfi.getEntityName());
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java

@ -24,7 +24,7 @@ import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Schema
@Schema(hidden = true)
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Deprecated

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serializable;
@Schema
@Schema(hidden = true)
@Data
@Deprecated
public class AlarmConditionFilter implements Serializable {

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

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.io.Serializable;
@Schema
@Schema(hidden = true)
@Data
@Deprecated
public class AlarmRule implements Serializable {

9
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java

@ -18,20 +18,13 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.io.Serializable;
@Schema(
description = "Configuration for alarm schedule",
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "ANY_TIME", schema = AnyTimeSchedule.class),
@DiscriminatorMapping(value = "SPECIFIC_TIME", schema = SpecificTimeSchedule.class),
@DiscriminatorMapping(value = "CUSTOM", schema = CustomTimeSchedule.class)
}
hidden = true
)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmScheduleType.java

@ -15,6 +15,9 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(hidden = true)
@Deprecated
public enum AlarmScheduleType {

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java

@ -15,8 +15,10 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.query.DynamicValue;
@Schema(hidden = true)
@Deprecated
public class AnyTimeSchedule implements AlarmSchedule {

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.util.List;
@Schema(hidden = true)
@Data
@Deprecated
public class CustomTimeSchedule implements AlarmSchedule {

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeScheduleItem.java

@ -15,10 +15,12 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Schema(hidden = true)
@Data
@Deprecated
public class CustomTimeScheduleItem implements Serializable {

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import lombok.Data;
@ -37,7 +36,7 @@ public class DeviceProfileData implements Serializable {
@Schema(description = "JSON object of provisioning strategy type per device profile")
private DeviceProfileProvisionConfiguration provisionConfiguration;
@Valid
@ArraySchema(schema = @Schema(implementation = DeviceProfileAlarm.class))
@Schema(hidden = true)
private List<DeviceProfileAlarm> alarms;
}

2
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.common.data.device.profile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.util.Set;
@Schema(hidden = true)
@Data
@Deprecated
public class SpecificTimeSchedule implements AlarmSchedule {

1
common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java

@ -46,7 +46,6 @@ public abstract class TaskResult {
@JsonIgnore
public abstract JobType getJobType();
@JsonIgnore
public abstract String getError();
}

2
common/data/src/main/java/org/thingsboard/server/common/data/page/SortOrder.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.page;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@ -36,6 +37,7 @@ public class SortOrder {
return new SortOrder(property, direction);
}
@Schema(name = "SortOrderDirection")
public static enum Direction {
ASC, DESC
}

3
common/data/src/main/java/org/thingsboard/server/common/data/query/ComplexOperation.java

@ -15,6 +15,9 @@
*/
package org.thingsboard.server.common.data.query;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "AlarmRuleComplexOperation")
public enum ComplexOperation {
AND,
OR

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

@ -41,7 +41,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo<RuleChainId> implement
private static final long serialVersionUID = -5656679015121935465L;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY)
@Schema(description = "JSON object with Tenant Id.", accessMode = Schema.AccessMode.READ_ONLY)
private TenantId tenantId;
@NoXss
@Length(fieldName = "name")

2
common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.transport.resource;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "TransportResourceType")
@Schema(hidden = true)
public enum ResourceType {
LWM2M_MODEL, JKS, PKCS_12
}

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

@ -201,18 +201,6 @@ public final class TbMsg implements Serializable {
return builder.build();
}
@Deprecated(forRemoval = true, since = "4.1") // to be removed in 4.2
public static TbMsg fromProto(String queueName, TbMsgProto proto, ByteString data, TbMsgCallback callback) {
try {
if (!data.isEmpty()) {
proto = TbMsgProto.parseFrom(data);
}
} catch (InvalidProtocolBufferException e) {
throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
}
return fromProto(queueName, proto, callback);
}
public static TbMsg fromProto(String queueName, TbMsgProto proto, TbMsgCallback callback) {
TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));

1
common/proto/src/main/proto/queue.proto

@ -139,7 +139,6 @@ message SessionInfoProto {
message RestApiCallResponseMsgProto {
int64 requestIdMSB = 1;
int64 requestIdLSB = 2;
bytes response = 5 [deprecated = true];
msgqueue.TbMsgProto responseProto = 6;
}

142
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java

@ -245,7 +245,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
List<EfentoTelemetry> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray());
String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNumber().toByteArray());
boolean batteryStatus = protoMeasurements.getBatteryStatus();
int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase();
int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor();
@ -258,6 +258,9 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
Map<Long, JsonObject> valuesMap = new TreeMap<>();
// general measurements per message
valuesMap.put(TimeUnit.SECONDS.toMillis(channelsList.get(0).getTimestamp()), CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, nextTransmissionAtMillis, signal));
for (int channel = 0; channel < channelsList.size(); channel++) {
ProtoChannel protoChannel = channelsList.get(channel);
List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList();
@ -271,6 +274,10 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp());
// measurements per channel
JsonObject tsValues = valuesMap.computeIfAbsent(startTimestampMillis, k -> new JsonObject());
tsValues.addProperty("measurement_interval_" + (channel + 1), measurementPeriod);
for (int i = 0; i < sampleOffsetsList.size(); i++) {
int sampleOffset = sampleOffsetsList.get(i);
if (isSensorError(sampleOffset)) {
@ -290,22 +297,16 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset);
long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis);
values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> new JsonObject());
addBinarySample(protoChannel, currentIsOk, values, channel + 1, sessionId);
} else {
long timestampMillis = startTimestampMillis + i * measurementPeriodMillis;
values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements(
serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values = valuesMap.computeIfAbsent(timestampMillis, k -> new JsonObject());
addContinuesSample(protoChannel, sampleOffset, values, channel + 1, sessionId);
}
}
}
if (CollectionUtils.isEmpty(valuesMap)) {
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
}
return valuesMap.entrySet().stream()
.map(entry -> new EfentoTelemetry(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
@ -446,7 +447,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {
EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {
JsonObject values = new JsonObject();
values.addProperty("sw_version", protoDeviceInfo.getSwVersion());
@ -476,41 +477,86 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
//modem info
DeviceInfoProtos.ProtoModem modem = protoDeviceInfo.getModem();
values.addProperty("modem_types", modem.getType().toString());
values.addProperty("sc_EARNFCN_offset", modem.getParameters(0));
values.addProperty("sc_EARFCN", modem.getParameters(1));
values.addProperty("sc_PCI", modem.getParameters(2));
values.addProperty("sc_Cell_id", modem.getParameters(3));
values.addProperty("sc_RSRP", modem.getParameters(4));
values.addProperty("sc_RSRQ", modem.getParameters(5));
values.addProperty("sc_RSSI", modem.getParameters(6));
values.addProperty("sc_SINR", modem.getParameters(7));
values.addProperty("sc_Band", modem.getParameters(8));
values.addProperty("sc_TAC", modem.getParameters(9));
values.addProperty("sc_ECL", modem.getParameters(10));
values.addProperty("sc_TX_PWR", modem.getParameters(11));
values.addProperty("op_mode", modem.getParameters(12));
values.addProperty("nc_EARFCN", modem.getParameters(13));
values.addProperty("nc_EARNFCN_offset", modem.getParameters(14));
values.addProperty("nc_PCI", modem.getParameters(15));
values.addProperty("nc_RSRP", modem.getParameters(16));
values.addProperty("RLC_UL_BLER", modem.getParameters(17));
values.addProperty("RLC_DL_BLER", modem.getParameters(18));
values.addProperty("MAC_UL_BLER", modem.getParameters(19));
values.addProperty("MAC_DL_BLER", modem.getParameters(20));
values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21));
values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22));
values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23));
values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24));
values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25));
values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26));
values.addProperty("RLC_UL_tput", modem.getParameters(27));
values.addProperty("RLC_DL_tput", modem.getParameters(28));
values.addProperty("MAC_UL_tput", modem.getParameters(29));
values.addProperty("MAC_DL_tput", modem.getParameters(30));
values.addProperty("sleep_duration", modem.getParameters(31));
values.addProperty("rx_time", modem.getParameters(32));
values.addProperty("tx_time", modem.getParameters(33));
DeviceInfoProtos.ModemType modemType = modem.getType();
values.addProperty("modem_types", modemType.toString());
values.addProperty("sim_card_identification", modem.getSimCardIdentification());
values.addProperty("firmware_version", modem.getFirmwareVersion().toString());
values.addProperty("modem_identification", modem.getModemIdentification());
if (modem.getModemStatisticsCount() >= 4) {
values.addProperty("modem_transmissions_count", modem.getModemStatistics(0));
values.addProperty("modem_time_since_last_devinfo", modem.getModemStatistics(1));
values.addProperty("modem_total_psm_time", modem.getModemStatistics(2));
values.addProperty("modem_total_active_time", modem.getModemStatistics(3));
}
switch (modemType) {
case MODEM_TYPE_BC660:
values.addProperty("sc_EARFCN", modem.getParameters(0));
values.addProperty("sc_EARNFCN_offset", modem.getParameters(1));
values.addProperty("sc_PCI", modem.getParameters(2));
values.addProperty("sc_Cell_id", modem.getParameters(3));
values.addProperty("sc_RSRP", modem.getParameters(4));
values.addProperty("sc_RSRQ", modem.getParameters(5));
values.addProperty("sc_RSSI", modem.getParameters(6));
values.addProperty("sc_SINR", modem.getParameters(7));
values.addProperty("sc_Band", modem.getParameters(8));
values.addProperty("sc_TAC", modem.getParameters(9));
values.addProperty("sc_ECL", modem.getParameters(10));
values.addProperty("sc_TX_PWR", modem.getParameters(11));
values.addProperty("op_mode", modem.getParameters(12));
values.addProperty("nc_EARFCN", modem.getParameters(13));
values.addProperty("nc_PCI", modem.getParameters(14));
values.addProperty("nc_RSRP", modem.getParameters(15));
values.addProperty("nc_RSRQ", modem.getParameters(16));
values.addProperty("sleep_duration", modem.getParameters(17));
values.addProperty("rx_time", modem.getParameters(18));
values.addProperty("tx_time", modem.getParameters(19));
values.addProperty("PLMN_state", modem.getParameters(20));
values.addProperty("select_PLMN", modem.getParameters(21));
break;
case MODEM_TYPE_SHARED_MODEM:
values.addProperty("RSRP", modem.getParameters(0));
values.addProperty("RSRQ", modem.getParameters(1));
values.addProperty("RSSI", modem.getParameters(2));
values.addProperty("SINR", modem.getParameters(3));
break;
default:
// MODEM_TYPE_UNSPECIFIED, MODEM_TYPE_BC66, MODEM_TYPE_BC66NA
values.addProperty("sc_EARNFCN_offset", modem.getParameters(0));
values.addProperty("sc_EARFCN", modem.getParameters(1));
values.addProperty("sc_PCI", modem.getParameters(2));
values.addProperty("sc_Cell_id", modem.getParameters(3));
values.addProperty("sc_RSRP", modem.getParameters(4));
values.addProperty("sc_RSRQ", modem.getParameters(5));
values.addProperty("sc_RSSI", modem.getParameters(6));
values.addProperty("sc_SINR", modem.getParameters(7));
values.addProperty("sc_Band", modem.getParameters(8));
values.addProperty("sc_TAC", modem.getParameters(9));
values.addProperty("sc_ECL", modem.getParameters(10));
values.addProperty("sc_TX_PWR", modem.getParameters(11));
values.addProperty("op_mode", modem.getParameters(12));
values.addProperty("nc_EARFCN", modem.getParameters(13));
values.addProperty("nc_EARNFCN_offset", modem.getParameters(14));
values.addProperty("nc_PCI", modem.getParameters(15));
values.addProperty("nc_RSRP", modem.getParameters(16));
values.addProperty("RLC_UL_BLER", modem.getParameters(17));
values.addProperty("RLC_DL_BLER", modem.getParameters(18));
values.addProperty("MAC_UL_BLER", modem.getParameters(19));
values.addProperty("MAC_DL_BLER", modem.getParameters(20));
values.addProperty("MAC_UL_TOTAL_BYTES", modem.getParameters(21));
values.addProperty("MAC_DL_TOTAL_BYTES", modem.getParameters(22));
values.addProperty("MAC_UL_total_HARQ_Tx", modem.getParameters(23));
values.addProperty("MAC_DL_total_HARQ_Tx", modem.getParameters(24));
values.addProperty("MAC_UL_HARQ_re_Tx", modem.getParameters(25));
values.addProperty("MAC_DL_HARQ_re_Tx", modem.getParameters(26));
values.addProperty("RLC_UL_tput", modem.getParameters(27));
values.addProperty("RLC_DL_tput", modem.getParameters(28));
values.addProperty("MAC_UL_tput", modem.getParameters(29));
values.addProperty("MAC_DL_tput", modem.getParameters(30));
values.addProperty("sleep_duration", modem.getParameters(31));
values.addProperty("rx_time", modem.getParameters(32));
values.addProperty("tx_time", modem.getParameters(33));
break;
}
//Runtime info
DeviceInfoProtos.ProtoRuntime runtimeInfo = protoDeviceInfo.getRuntimeInfo();
@ -521,7 +567,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
values.addProperty("counter_of_non_confirmable_messages_attempts", runtimeInfo.getMessageCounters(1));
values.addProperty("counter_of_succeeded_messages", runtimeInfo.getMessageCounters(2));
values.addProperty("min_battery_mcu_temp", runtimeInfo.getMinBatteryMcuTemperature());
values.addProperty("min_battery_voltage", runtimeInfo.getMinBatteryVoltage());
values.addProperty("min_battery_voltage", runtimeInfo.getBatteryVoltage());
values.addProperty("min_mcu_temp", runtimeInfo.getMinMcuTemperature());
values.addProperty("runtime_errors", runtimeInfo.getRuntimeErrorsCount());
values.addProperty("up_time", runtimeInfo.getUpTime());
@ -529,8 +575,8 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
return new EfentoTelemetry(System.currentTimeMillis(), values);
}
private JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException {
return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(2)));
JsonElement getEfentoConfiguration(byte[] bytes) throws InvalidProtocolBufferException {
return parseString(ProtoConverter.dynamicMsgToJson(bytes, ConfigProtos.getDescriptor().getMessageTypes().get(0)));
}
private static String getDate(long seconds) {

4
common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java

@ -59,14 +59,12 @@ public class CoapEfentoUtils {
return String.format("%s UTC", simpleDateFormat.format(new Date(timestampInMillis)));
}
public static JsonObject setDefaultMeasurements(String serialNumber, boolean batteryStatus, long measurementPeriod, long nextTransmissionAtMillis, long signal, long startTimestampMillis) {
public static JsonObject setDefaultMeasurements(String serialNumber, boolean batteryStatus, long nextTransmissionAtMillis, long signal) {
JsonObject values = new JsonObject();
values.addProperty("serial", serialNumber);
values.addProperty("battery", batteryStatus ? "ok" : "low");
values.addProperty("measured_at", convertTimestampToUtcString(startTimestampMillis));
values.addProperty("next_transmission_at", convertTimestampToUtcString(nextTransmissionAtMillis));
values.addProperty("signal", signal);
values.addProperty("measurement_interval", measurementPeriod);
return values;
}

673
common/transport/coap/src/main/proto/efento/proto_config.proto

@ -15,338 +15,471 @@
*/
syntax = "proto3";
import "efento/proto_measurement_types.proto";
import "efento/proto_rule.proto";
import "efento/proto_config_types.proto";
import "efento/proto_measurement_types.proto";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "ConfigProtos";
/* Message containing optional channels control parameters */
message ProtoOutputControlState {
/* Channel index */
uint32 channel_index = 1;
/* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */
uint32 channel_state = 2;
}
/* Message containing request data for accessing calibration parameters */
message ProtoCalibrationParameters {
/* Request details. Bitmask: */
/* - calibration_request[0:2] - requested channel number. */
uint32 calibration_request = 1;
/* Assignment of a channel. */
uint32 channel_assignment = 2;
/* Table of calibration parameters. Max size = 8. */
repeated int32 parameters = 3;
}
enum BleAdvertisingPeriodMode {
/* Invalid value */
BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0;
message ProtoConfig {
/* Default behavior - faster advertising when measurement period is < 15s. */
BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1;
/* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
/* User-configured normal interval is used. */
BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2;
reserved 1,48;
/* User-configured fast interval is used. */
BLE_ADVERTISING_PERIOD_MODE_FAST = 3;
}
/* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
/* Message containing BLE advertising period configuration */
message ProtoBleAdvertisingPeriod {
/* Serial number of the device. *
* Length: 6 bytes. *
* This field is only sent by the device. *
* Status: In use [06.00 - LATEST] */
bytes serial_number = 25;
/* BLE advertising mode: */
/* - 1: Default, BLE advertising interval is set to 1022.5ms or some lower value, based on continuous measurement period. */
/* - 2: Normal, uses user-configured value from 'normal' field. */
/* - 3: Fast, uses user-configured value from 'fast' field (must be lower than or equal to 'normal' field). */
BleAdvertisingPeriodMode mode = 1;
/* Configuration payload split information: *
* - Values < 0 - Payload split, expect another part of the payload in the next message. *
* The absolute value indicates an index of the current message *
* - Values = 0 - Payload not split *
* - Values > 0 - Last part of the split payload, the value indicates the total number of the messages sent *
* This field is only sent by the device. *
* Status: In use [06.08.00 - LATEST] */
sint32 payload_split_info = 44;
/* BLE advertising interval when in normal mode, configured in 0.625ms steps. */
/* Range: [32:16384] */
uint32 normal = 2;
/* Identifier of the current configuration. *
* The value of this field changes with every configuration change. *
* This field is only sent by the device. *
* Status: In use [07.00.00 - LATEST] / Previously as hash [06.00 - 06.xx.xx] */
uint32 configuration_hash = 21;
/* Timestamp when the new configuration was set. *
* This field is only sent by the device. *
* Status: In use [07.00.00 - LATEST] / Previously as hash_timestamp [06.02 - 06.xx.xx] */
uint32 configuration_hash_timestamp = 39;
/* Configuration errors. *
* Up to 20 error codes supported. *
* This field is only sent by the device. *
* Status: In use [07.00.00 - LATEST] / Previously as errors [06.00 - 06.xx.xx] */
repeated uint32 configuration_errors = 20;
/* Timestamp when a new configuration error was reported. *
* This field is only sent by the device. *
* Status: In use [07.00.00 - LATEST] / Previously as timestamp [06.02 - 06.xx.xx] */
uint32 configuration_error_timestamp = 38;
/* Measurement channel types. *
* This field is only sent by the device. *
* Status: In use [06.00 - LATEST] */
repeated MeasurementType channel_types = 27;
/* BLE advertising interval when in fast mode, configured in 0.625ms steps. */
/* Range: [32:16384] */
uint32 fast = 3;
}
/* NvM status: *
* - 1 - Defaults restored on CRC error: default sensor configuration restored due to the non-volatile memory configuration *
* data integrity failure *
* - 2 - NvM initialization error: changes in the sensor configuration won't be stored in the non-volatile memory, a power *
* reset of the sensor is required *
* When empty field is sent by the sensor NvM status is OK. *
* NvM status can be cleared by sending to the sensor value 0x7F. *
* Status: In use [07.00.00 - LATEST] */
uint32 nvm_status = 62;
/* SERVER STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
/* Current time in seconds since 1st of January 1970 (epoch time). *
* This field is only sent by the server. *
* Status: In use [06.00 - LATEST] */
uint32 current_time = 8;
/* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */
/* changed need to be sent in the payload. */
/* If the value of a selected parameter shall not be changed, do not include it in the payload */
message ProtoConfig {
/* REQUEST FIELDS ----------------------------------------------------------------------------------------------------------- */
/* Specifies whether the device should accept the configuration without functional testing (e.g., network connection). *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as accept_without_testing [06.00 - 06.xx.xx] */
bool accept_without_testing_request = 22;
/* Specifies whether to send the configuration from the sensor to the configuration endpoint. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as request_configuration [06.00 - 06.xx.xx] */
bool configuration_request = 19;
/* Specifies whether to send the device information from the sensor to the device information endpoint. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as request_device_info [06.00 - 06.xx.xx] */
bool device_info_request = 6;
/* Specifies whether to send the extended configuration from the sensor to the extended configuration endpoint. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] */
bool extended_configuration_request = 63;
/* Specifies, if software update is available. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as request_fw_update [06.00 - 06.xx.xx] */
bool update_software_request = 7;
/* Device will clear all runtime errors. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as request_runtime_errors_clear [06.02 - 06.xx.xx] */
bool clear_runtime_errors_request = 37;
/* Device will power off its cellular modem for requested number of seconds. *
* Range: [60:604800] (1 minute : 7 days) *
* This field is only sent by the user/server. *
* Status: In use [06.00 - LATEST] */
uint32 disable_modem_request = 18;
/* DEPRECATED - Used for backward compatibility with fw versions 5.x */
/* repeated Threshold thresholds = 1; */
/* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */
/* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
/* Sensors of 'Binary' type take measurement each Measurement_period_base. */
/* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */
/* not sent (equal to 0), then the default value '14' shall be used for period calculation. */
/* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */
/* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
/* measurement period base in seconds */
/* Range [1:65535] - minimum value can vary depends on installed sensors */
/* Specifies, if the modem firmware update is available. *
* String up to 48 characters: *
* - DFOTA URL - For use by BC66/BC660 modem. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as modem_update_request [06.08.00 - 06.xx.xx] */
string update_modem_request = 45;
/* Device will erase all measurements from memory. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as memory_reset_request [06.00 - 06.xx.xx] */
bool reset_memory_request = 30;
/* Device will restart the collection of memory statistics. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] */
bool restart_memory_stats_request = 64;
/* Specifies whether to send measurements from the sensor starting at the specified timestamp. *
* Tiemstamp in seconds since 1st of January 1970 (epoch time). *
* All previous measurements will be marked as sent. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] */
uint32 data_transfer_start_timestamp_request = 65;
/* Device will set new calibration parameters or will send the current set of parameters to the configuration endpoint. *
* Status: Deprecated [06.10.00 - 06.xx.xx] */
ProtoCalibrationParametersRequest calibration_parameters_request = 49;
/* Device will set the new output state of the output control channel pins. *
* Up to 3 channels supported in the one request. *
* This field is only sent by the user/server. *
* Status: In use [07.00.00 - LATEST] / Previously as output_control_state_request [06.13.00/06.21.00 - 06.xx.xx] */
repeated ProtoOutputControlState set_output_control_state_request = 58;
/* MEASUREMENTS CONFIGURATION ----------------------------------------------------------------------------------------------- */
/* Measurement period defines how often the measurements are to be taken. *
* Sensors of 'Continuous' type take measurement each 'measurement_period_base' * 'measurement_period_factor'. *
* Sensors of 'Binary' type take measurement each 'measurement_period_base'. *
* For backward compatibility with versions 5.xx in case of 'Binary/Mixed' sensors, if the 'measurement_period_factor' is *
* not sent (equal to 0), then the default value '14' shall be used for the period calculation. *
* For backward compatibility with versions 5.xx in case of 'Continuous' sensors, if the 'measurement_period_factor' is *
* not sent (equal to 0), then the default value '1' shall be used for the period calculation. */
/* Measurement period base in seconds. *
* Range: [1:65535] (minimum value may vary depending on sensors installed) *
* Group: Measurement Period *
* Status: In use [06.00 - LATEST] */
uint32 measurement_period_base = 2;
/* Measurement period factor */
/* Range [1:65535] - minimum value can vary depends on installed sensors */
/* Measurement period factor. *
* Range: [1:65535] (minimum value may vary depending on sensors installed) *
* Group: Measurement Period *
* Status: In use [06.00 - LATEST] */
uint32 measurement_period_factor = 26;
/* Transmission interval in seconds. Range: [60:604800] */
uint32 transmission_interval = 3;
/* BLUETOOTH CONFIGURATION -------------------------------------------------------------------------------------------------- */
/* BLE turnoff time in seconds. Once receiving this setting, BLE will be switched off after the set number of seconds. */
/* If BLE is already switched off, it will switch on for the set number of seconds and switch off afterwards. */
/* Range [60:604800] and 0xFFFFFFFF */
/* 0xFFFFFFFF - always on */
/* Bluetooth turn-off time: *
* - [60:604800] - Time in seconds after which Bluetooth is turned off *
* - 0xFFFFFFFF - Bluetooth is always on *
* When this setting is received, Bluetooth is turned off after the set number of seconds. *
* If Bluetooth is already off, it will turn on for the set number of seconds and then turn off. *
* Group: Bluetooth Turn-Off *
* Status: In use [06.00 - LATEST] */
uint32 ble_turnoff_time = 4;
/* ACK interval in seconds */
/* Range [180:2592000] and 0xFFFFFFFF */
/* 0xFFFFFFFF - always request ACK */
uint32 ack_interval = 5;
/* Specifies, if the additional device info is requested. If true, sensor will send a message to endpoint '/i' with the */
/* device info. This field is only sent by server */
bool request_device_info = 6;
/* Specifies, if software update is available. This field is only sent by server */
bool request_fw_update = 7;
/* Current time in seconds sine 1st of January 1970 (epoch time). */
uint32 current_time = 8;
/* NB-IoT transfer limit */
/* Range: [1:65535] */
/* 65535 - disable transfer limit function */
uint32 transfer_limit = 9;
/* NB-IoT transfer limit timer in seconds */
/* Range: [1:65535] */
/* 65535 - disable transfer limit function */
uint32 transfer_limit_timer = 10;
/* For firmware >= 6.07.00: */
/* IP or URL address of the data (measurements) server */
/* The IP or URL of the data server, provided as string with a maximum length of 31 characters */
/* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
/* For firmware < 6.07.00: */
/* IP address of the data (measurements) server */
/* For example, use "18.184.24.239" */
string data_server_ip = 11;
/* Data (measurements) server port */
/* Range: [1:65535] */
uint32 data_server_port = 12;
/* For firmware >= 6.07.00: */
/* IP or URL address of the update server */
/* The IP or URL of the update server, provided as string with a maximum length of 31 characters */
/* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
/* For firmware < 6.07.00: */
/* IP address of the update server */
/* For example, use "18.184.24.239" */
string update_server_ip = 13;
/* Update server port for UDP transmission */
/* Range: [1:65535] */
uint32 update_server_port_udp = 14;
/* Update server port for CoAP transmission */
/* Range: [1:65535] */
uint32 update_server_port_coap = 15;
/* APN as string. Max length 49 */
/* String with special character 0x7F (DEL) only indicates that automatic apn is turn on */
string apn = 16;
/* Bluetooth Tx power level. *
* Value is the index of the absolute value of the Tx power, which depends on the BLE module. *
* Range: [1:4] *
* Group: Bluetooth Tx Power *
* Status: In use [06.02 - LATEST] */
uint32 ble_tx_power_level = 36;
/* PLMN selection */
/* Range: [100:999999] */
/* 0xFFFFFFFF or 1000000 - automatic selection */
uint32 plmn_selection = 17;
/* Encryption key. *
* Max length: 16 bytes. *
* A one-element array containing only one 0x7F (DEL) byte disables encryption. *
* Device sends two last bytes of SHA256 hash of current key in this field. *
* When the encryption key is disabled, the device sends a 0x7F (DEL) byte. *
* Group: Encryption *
* Status: In use [06.11.00 - LATEST] */
bytes encryption_key = 54;
/* Device will power off its cellular modem for requested number of seconds. */
/* Range: [60:604800] (1 minute : 7 days) */
/* This field is only sent by server */
uint32 disable_modem_request = 18;
/* Bluetooth advertising period. *
* Group: Bluetooth Advertising Period *
* Status: In use [06.13.00/06.21.00 - LATEST] */
ProtoBleAdvertisingPeriod ble_advertising_period = 59;
/* If set, the device will send its configuration to the endpoint '/c' as a confirmable message */
/* This field is only sent by server */
bool request_configuration = 19;
/* Advertisement manufacturer specific data format. *
* Group: Advertisement Manufacturer Data Format *
* Status: In use [07.00.00 - LATEST] */
AdvertisementManufacturerDataFormat advertisement_manufacturer_data_format = 60;
/* Device's error codes. */
/* This field is only sent by device */
repeated uint32 errors = 20;
/* EDGE LOGIC CONFIGURATION ------------------------------------------------------------------------------------------------- */
/* Identifier of current configuration - Every change of the configuration results in change of the value of this field */
/* This field is only sent by device */
uint32 hash = 21;
/* Edge logic rules set on the device. *
* Up to 16 rules supported (previously 12 rules [06.00 - 07.01.xx]). *
* Group: Edge Logic Rule No. # *
* Status: In use [06.00 - LATEST] */
repeated ProtoRule rules = 28;
/* If true, the device will accept the configuration without functional testing (eg. network connection) */
bool accept_without_testing = 22;
/* Calendars set on the device. *
* Up to 6 calendars supported. *
* Group: Calendar No. # *
* Status: In use [06.08.00 - LATEST] */
repeated ProtoCalendar calendars = 47;
/* Cloud token configuration: */
/* - 1: cloud token set to the value of cloud_token field */
/* - 2: cloud token set to IMEI of the cellular module */
/* - 255: do not send cloud_token field */
uint32 cloud_token_config = 23;
/* SERVER COMMUNICATION CONFIGURATION --------------------------------------------------------------------------------------- */
/* Cloud token that should be sent with each measurement frame */
string cloud_token = 24;
/* Transmission interval in seconds. *
* Range: [60:604800] *
* Group: Server Intervals *
* Status: In use [06.00 - LATEST] */
uint32 transmission_interval = 3;
/* Serial number of the device */
/* This field is only sent by device */
bytes serial_number = 25;
/* ACK interval: *
* - [180:2592000] - Time in seconds after which the device will request an ACK *
* - 0xFFFFFFFF - Always request ACK *
* Group: Server Intervals *
* Status: In use [06.00 - LATEST] */
uint32 ack_interval = 5;
/* Type of channel */
/* This field is only sent by device */
repeated MeasurementType channel_types = 27;
/* Server transfer limit: *
* - [1:65534] - Number of transfers *
* - 65535 - Transfer limit disabled *
* Group: Server Transfer Limit *
* Status: In use [06.00 - LATEST] */
uint32 transfer_limit = 9;
/* Edge logic rules set on the device. Up to 12 rules are supported */
repeated ProtoRule rules = 28;
/* Server transfer limit timer: *
* - [1:65534] - Time in seconds after which the transfer is renewed *
* - 65535 - Transfer limit disabled *
* Group: Server Transfer Limit *
* Status: In use [06.00 - LATEST] */
uint32 transfer_limit_timer = 10;
/* Supervision period */
/* Range: [180:604800] */
/* 0xFFFFFFFF - Functionality disabled */
/* Server supervision period: *
* - [180:604800] - Time in seconds after which the device resets itself if there is no communication with the server *
* - 0xFFFFFFFF - Server supervision disabled *
* Group: Server Supervision *
* Status: In use [06.00 - LATEST] */
uint32 supervision_period = 29;
/* If true, sensor's measurement memory will be erased */
bool memory_reset_request = 30;
/* DATA SERVER CONFIGURATION ------------------------------------------------------------------------------------------------ */
/* Bytes 0-4 - Band selection mask. Mask = 1 << position */
/* Band | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | */
/* Position: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | */
/* example: To enable band 3, 8 and 20 set to (1 << 2) + (1 << 5) + (1 << 11) = 2084 */
uint32 modem_bands_mask = 31;
/* Data server address. *
* String up to 31 characters: *
* - IPv4 address (examples: "18.184.24.239") [06.00 - 06.06.xx] *
* - IPv4/URL address (examples: "18.184.24.239", "efento.test.io") [06.07.00 - LATEST] *
* Group: Data Server *
* Status: In use [06.00 - LATEST] */
string data_server_ip = 11;
/* Data server port. *
* Range: [1:65535] *
* Group: Data Server *
* Status: In use [06.00 - LATEST] */
uint32 data_server_port = 12;
/* Data endpoint (string - max length 16) */
/* Data endpoint. *
* String up to 11 characters. *
* Group: Data Server *
* Status: In use [06.02 - LATEST] */
string data_endpoint = 32;
/* Configuration endpoint (string - max length 16) */
/* Configuration endpoint. *
* String up to 11 characters. *
* Group: Data Server *
* Status: In use [06.02 - LATEST] */
string configuration_endpoint = 33;
/* Device info endpoint (string - max length 16) */
/* Extended configuration endpoint. *
* String up to 11 characters. *
* Group: Data Server *
* Status: In use [07.00.00 - LATEST] */
string extended_configuration_endpoint = 61;
/* Device information endpoint. *
* String up to 11 characters. *
* Group: Device Information Endpoint *
* Status: In use [06.02 - LATEST] */
string device_info_endpoint = 34;
/* Time endpoint (string - max length 16) */
/* Time endpoint. *
* String up to 11 characters. *
* Group: Time Endpoint *
* Status: In use [06.02 - LATEST] */
string time_endpoint = 35;
/* Bluetooth TX power level. Value is the index of the absolute value of TX power, that depends on the BLE module */
/* Range: [1:4] */
uint32 ble_tx_power_level = 36;
/* UPDATE SERVER CONFIGURATION ---------------------------------------------------------------------------------------------- */
/* Deprecated field */
/* If true, the sensor's runtime errors will be cleared */
bool request_runtime_errors_clear = 37;
/* Update server address. *
* String up to 31 characters: *
* - IPv4 address (examples: "18.184.24.239") [06.00 - 06.06.xx] *
* - IPv4/URL address (examples: "18.184.24.239", "efento.test.io") [06.07.00 - LATEST] *
* Group: Update Server *
* Status: In use [06.00 - LATEST] */
string update_server_ip = 13;
/* Timestamp when a new error code was reported */
uint32 error_timestamp = 38;
/* Update server port for UDP transfer. *
* Range: [1:65535] *
* Group: Update Server *
* Status: In use [06.00 - LATEST] */
uint32 update_server_port_udp = 14;
/* Timestamp when the new configuration was set */
uint32 hash_timestamp = 39;
/* Update server port for CoAP transfer. *
* Range: [1:65535] *
* Group: Update Server *
* Status: In use [06.00 - LATEST] */
uint32 update_server_port_coap = 15;
/* Cloud token CoAP option ID: */
/* - [1:64999] - CoAP option ID containing cloud token */
/* - 65000 - cloud token sent in the payload */
uint32 cloud_token_coap_option = 40;
/* CLOUD CONFIGURATION ------------------------------------------------------------------------------------------------------ */
/* ECDSA payload signature CoAP option ID: */
/* - [1:64999] - CoAP option ID containing ECDSA payload signature */
/* - 65000 - no payload signature in CoAP option */
uint32 payload_signature_coap_option = 41;
/* Cloud token configuration: *
* - 1 - Cloud token set to the value of the 'cloud_token' field *
* - 2 - Cloud token set to the modem identification (IMEI for cellular modems) *
* - 255 - Do not send 'cloud_token' field *
* Group: Cloud Token *
* Status: In use [06.00 - LATEST] */
uint32 cloud_token_config = 23;
/* DNS server IP address grouped in the array as four octets. Set 255.255.255.255 to use a network DNS server */
/* Note: when setting less than four octets the remaining will be filled with zeros. */
repeated uint32 dns_server_ip = 42;
/* Cloud token that should be sent with each measurement frame. *
* String up to 36 characters. *
* Group: Cloud Token *
* Status: In use [06.00 - LATEST] */
string cloud_token = 24;
/* DNS TTL configuration: */
/* - [1:864000] - custom TTL in seconds (additionally, the DNS request when communication has failed) */
/* - 864001 - accept TTL from the DNS server (additionally, the DNS request when communication has failed) */
/* - 864002 - DNS request is only after communication failed */
uint32 dns_ttl_config = 43;
/* Cloud token CoAP option ID: *
* - [1:64999] - CoAP option ID with cloud token *
* - 65000 - Cloud token sent in the payload *
* Group: Cloud Token *
* Status: In use [06.07.00 - LATEST] */
uint32 cloud_token_coap_option = 40;
/* Configuration payload split information. Information about dividing the payload into parts */
/* values < 0 - payload has been split, expect another part of the payload in the next message. */
/* The absolute value indicates an index of the current message. */
/* value = 0 - payload has not been splitted */
/* values > 0 - last part of the split payload, the value indicates the total number of the messages sent */
sint32 payload_split_info = 44;
/* ECDSA payload signature CoAP option ID: *
* - [1:64999] - CoAP option ID with payload signature *
* - 65000 - Payload signature is not sent *
* Group: Cloud Token *
* Status: In use [06.07.00 - LATEST] */
uint32 payload_signature_coap_option = 41;
/* Modem update request (string - max length 48) */
/* This field is only sent by server */
/* For BC66 module, this field is a DFOTA URL */
string modem_update_request = 45;
/* Modem identification CoAP option ID: *
* - [1:64999] - CoAP option ID with modem identification *
* - 65000 - Modem identification is not sent *
* Group: Cloud Token *
* Status: In use [07.00.00 - LATEST] */
uint32 modem_identification_coap_option = 52;
/* Cellular configuration parameters. */
/* 1st item - Number of used cellular parameters */
/* 2nd - 12th items - Cellular parameters */
repeated uint32 cellular_config_params = 46;
/* NETWORK CONFIGURATION ---------------------------------------------------------------------------------------------------- */
/* Calendar configuration. Up to 6 calendars are supported */
repeated ProtoCalendar calendars = 47;
/* DNS server IP address. *
* Grouped in the array as four octets. Set 255.255.255.255 to use a cellular network DNS server. *
* Note: when setting less than four octets the remaining will be filled with zeros. *
* Group: Data Server *
* Status: In use [06.07.00 - LATEST] */
repeated uint32 dns_server_ip = 42;
/* DEPRECATED - Used for backward compatibility */
reserved 48;
/* Set/get calibration parameters for single channel. */
ProtoCalibrationParameters calibration_parameters_request = 49;
/* LED behaviour configuration: */
/* Period of LEDs flashing (5-600 seconds in 5 seconds resolution): */
/* - led_config[0] - green LED */
/* - led_config[1] - red LED */
/* Time from entering the normal state, after which the LED indication is turned off */
/* (0-240 minutes in 1 minute resolution, or 255 for always turned on): */
/* - led_config[2] - flashing red led on communication problem */
/* - led_config[3] - flashing red led on a sensor problem */
/* - led_config[4] - flashing red led on a low power */
/* - led_config[5] - flashing green led on measurement */
/* - led_config[6] - flashing green led on transmission */
/* - led_config[7] - flashing green led to indicate sensor's proper operation */
/* - led_config[8] - Blink duration (20-1000ms in 5 ms resolution) */
repeated uint32 led_config = 50;
/* DNS TTL configuration: *
* - [1:864000] - Custom TTL in seconds (if communication fails, DNS is also queried) *
* - 864001 - Accept TTL from the DNS server (if communication fails, DNS is also queried) *
* - 864002 - DNS query only after communication failure *
* Group: DNS *
* Status: In use [06.07.00 - LATEST] */
uint32 dns_ttl_config = 43;
/* Network troubleshooting configuration, if bluetooth is turned off and communication with the server is faulty, */
/* bluetooth will be automatically turned on until the connection is stabilized */
/* - 1: network troubleshooting disabled */
/* - 2: network troubleshooting enabled */
/* Network troubleshooting:
* - 1 - Network troubleshooting disabled *
* - 2 - Network troubleshooting enabled *
* If Bluetooth is turned off and communication with the server is faulty, Bluetooth will be automatically turned on until *
* the connection is stabilized. *
* Group: Network *
* Status: In use [06.10.00 - LATEST] */
uint32 network_troubleshooting = 51;
/* Reserved by gateway client */
reserved 52, 53;
/* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */
/* encryption_key configuration. When encryption key is disabled one byte 0x7F (DEL) is sent. */
/* Max length: 16 bytes. */
/* 0x7F - encryption key disabled. */
bytes encryption_key = 54;
/* User name as string. Max length 31 */
/* String with special character 0x7F (DEL) only indicates that automatic user name is turn on */
/* User name can only be set to custom value if apn has been configured (is not automatic) */
string apn_user_name = 55;
/* Network key. *
* Max length: 16 bytes. *
* A one-element array containing only one 0x7F (DEL) byte disables network key. *
* Device sends two last bytes of SHA256 hash of current key in this field. *
* When the network key is disabled, the device sends a 0x7F (DEL) byte. *
* Group: Local Network *
* Status: In use [07.00.00 - LATEST] */
bytes network_key = 53;
/* MODEM CONFIGURATION ------------------------------------------------------------------------------------------------------ */
/* APN (Access Point Name). *
* String up to 49 characters. *
* A string containing only the special character 0x7F (DEL) indicates that automatic APN is enabled. *
* Group: Data Server *
* Status: In use [06.00 - LATEST] */
string apn = 16;
/* Password as string. Max length 31 */
/* String with special character 0x7F (DEL) only indicates that automatic password is turn on */
/* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */
/* APN username. *
* String up to 31 characters. *
* A string containing only the special character 0x7F (DEL) indicates that the username is not being used. *
* The username can only be set to a custom value if APN has been configured. *
* Group: Data Server *
* Status: In use [07.00.00 - LATEST] / Previously as apn_user_name [06.11.00 - 06.xx.xx] */
string apn_username = 55;
/* APN password. *
* String up to 31 characters. *
* A string containing only the special character 0x7F (DEL) indicates that the password is not being used. *
* The password can only be set to a custom value if APN username has been configured. *
* Group: Data Server *
* Status: In use [06.11.00 - LATEST] */
string apn_password = 56;
/* Reserved by versions above 06.20.00 */
reserved 57;
/* PLMN selection: *
* - [100:999999] - Selected operator code *
* - 1000000 - Automatic selection *
* - 0xFFFFFFFF - Automatic selection (legacy) *
* Group: Data Server *
* Status: In use [06.00 - LATEST] */
uint32 plmn_selection = 17;
/* Control output state on channel pin. Maximal number of requests equals 3 */
/* This field is only sent by server */
repeated ProtoOutputControlState output_control_state_request = 58;
/* Modem bands mask. *
* 4-byte bit mask, where each bit represents a specific band: *
* Band: | 1 | 2 | 3 | 4 | 5 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 25 | 26 | 28 | 66 | 71 | 85 | 70 | *
* Bit: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | *
* Example: To enable band 3, 8 and 20 set to 2084 = (1 << 2) + (1 << 5) + (1 << 11) *
* Group: Data Server *
* Status: In use [06.00 - LATEST] */
uint32 modem_bands_mask = 31;
/* BLE advertising period configuration. */
ProtoBleAdvertisingPeriod ble_advertising_period = 59;
}
/* Modem configuration parameters. *
* 1st item - The number of cellular parameters that are used (depending on the modem in use). *
* 2nd - 16th item - Cellular parameters. *
* Group: Cellular Configuration *
* Status: In use [06.08.00 - LATEST] */
repeated uint32 cellular_config_params = 46;
/* Network search schema. *
* Group: Network Search *
* Status: In use [06.20.00 - LATEST] */
ProtoNetworkSearch network_search = 57;
/* USER INTERFACE CONFIGURATION --------------------------------------------------------------------------------------------- */
/* LEDs behavior. *
* Period of LED blinking in 5-second unit (value is multiplied by 5). Range: [1:120]: *
* - 1st item - Period of GREEN LED blinking *
* - 2nd item - Period of RED LED blinking *
* Time elapsing from the entry into the normal state, after which the LED indicator is switched off, in minutes. *
* Range: [0:240; 255]. Value of 255 means always on: *
* - 3rd item - Blinking RED LED on communication problem *
* - 4th item - Blinking RED LED on a sensor problem *
* - 5th item - Blinking RED LED on a low power *
* - 6th item - Blinking GREEN LED on measurement *
* - 7th item - Blinking GREEN LED on transmission *
* - 8th item - Blinking GREEN LED to indicate sensor's proper operation *
* 9th item - Duration of LED blinking in 5-millisecond unit. Range: [4:200] *
* Group: LED *
* Status: In use [06.10.00 - LATEST] */
repeated uint32 led_config = 50;
}

125
common/transport/coap/src/main/proto/efento/proto_config_types.proto

@ -0,0 +1,125 @@
/**
* Copyright © 2016-2026 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.
*/
syntax = "proto3";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "ConfigTypesProtos";
message ProtoCalibrationParametersRequest {
/* Request details. *
* Bitmask: *
* - Bit 0:2 - Requested channel number, range: [1:6] *
* Status: Deprecated [06.10.00 - 06.xx.xx] */
uint32 calibration_request = 1;
/* Channel assignment - the sensor code. *
* Status: Deprecated [06.10.00 - 06.xx.xx] */
uint32 channel_assignment = 2;
/* Channel calibration parameters. *
* Up to 8 parameters supported. *
* If this field is empty, the sensor will send the current set of parameters. *
* Status: Deprecated [06.10.00 - 06.xx.xx] */
repeated int32 parameters = 3;
}
message ProtoOutputControlState {
/* Channel index. *
* Range: [0:5] *
* Status: In use [06.13.00/06.21.00 - LATEST] */
uint32 channel_index = 1;
/* Channel output state: *
* - 1 - OFF *
* - 2 - ON *
* Status: In use [06.13.00/06.21.00 - LATEST] */
uint32 channel_state = 2;
}
enum BleAdvertisingPeriodMode {
/* Invalid value. */
BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0;
/* Default mode. *
* Bluetooth advertising interval is set to 1022.5ms or a lower value, based on the continuous measurement period. */
BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1;
/* Normal mode. *
* Uses the value configured by the user from the 'normal' field. */
BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2;
/* Fast mode. *
* Uses the value configured by the user from the 'fast' field. */
BLE_ADVERTISING_PERIOD_MODE_FAST = 3;
}
message ProtoBleAdvertisingPeriod {
/* Bluetooth advertising mode. *
* Status: In use [06.13.00/06.21.00 - LATEST] */
BleAdvertisingPeriodMode mode = 1;
/* Bluetooth advertising interval in normal mode, configured in steps of 0.625 ms. *
* Range: [32:16384] *
* Status: In use [06.13.00/06.21.00 - LATEST] */
uint32 normal = 2;
/* Bluetooth advertising interval in fast mode, configured in steps of 0.625 ms. *
* Range: [32:16384] *
* Status: In use [06.13.00/06.21.00 - LATEST] */
uint32 fast = 3;
}
enum AdvertisementManufacturerDataFormat {
/* Invalid value. */
ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_UNSPECIFIED = 0;
/* Advertisement manufacturer specific data format 3. */
ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_V3 = 1;
/* Advertisement manufacturer specific data format 5. */
ADVERTISEMENT_MANUFACTURER_DATA_FORMAT_V5 = 2;
}
message ProtoNetworkSearch {
/* Timing schema, if successful registration since the last reset. *
* Length: 6 items. *
* 1st - 6th item - Time in minutes. Range: [1:255]. *
* Status: In use [06.20.00 - LATEST] */
repeated uint32 time_schema_last_registration_ok = 1;
/* Timing schema, if no successful registration since the last reset. *
* Length: 6 items. *
* 1st - 6th item - Time in minutes. Range: [1:255]. *
* Status: In use [06.20.00 - LATEST] */
repeated uint32 time_schema_last_registration_not_ok = 2;
/* Disable base period in minutes. *
* Disable time = 'disable_period_base' * counter (from 1 to 'counter_max'). *
* Range: [1:255] *
* Status: In use [06.20.00 - LATEST] */
uint32 disable_period_base = 3;
/* Disable counter maximum. *
* Range: [1:255] *
* Status: In use [06.20.00 - LATEST] */
uint32 counter_max = 4;
}

409
common/transport/coap/src/main/proto/efento/proto_device_info.proto

@ -18,185 +18,330 @@ syntax = "proto3";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "DeviceInfoProtos";
message ProtoRuntime
{
/* Up-time in seconds (since reset). *
* Status: In use [06.00 - LATEST] */
uint32 up_time = 1;
/* Message counters (since reset). *
* 1st item - Confirmable message attempts counter. *
* 2nd item - Non-confirmable message attempts counter. *
* 3rd item - Successful message counter. *
* Status: In use [06.00 - LATEST] */
repeated uint32 message_counters = 2;
/* MCU temperature in Celsius. *
* Status: In use [06.00 - LATEST] */
sint32 mcu_temperature = 3;
/* Battery voltage in mV. *
* - [0:65534] - Battery voltage in millivolts *
* - 65535 - No measurement *
* Status: In use [07.00.00 - LATEST] / Previously as min_battery_voltage [06.00 - 06.xx.xx] */
uint32 battery_voltage = 4;
/* MCU temperature in Celsius when minimum battery voltage was reached. *
* Status: Deprecated [06.00 - 06.xx.xx] */
sint32 min_battery_mcu_temperature = 5;
/* Battery reset timestamp in seconds since 1st of January 1970 (epoch time). *
* Status: Deprecated [06.00 - 06.xx.xx] */
uint32 battery_reset_timestamp = 6;
/* Maximum MCU temperature in Celsius. *
* Status: In use [06.00 - LATEST] */
sint32 max_mcu_temperature = 7;
/* Minimum MCU temperature in Celsius *
* Status: In use [06.00 - LATEST] */
sint32 min_mcu_temperature = 8;
/* Device's runtime errors. *
* Up to 20 error items supported. *
* Status: In use [06.02 - LATEST] */
repeated uint32 runtime_errors = 9;
/* Number of sensor resets (since power-up). *
* Status: In use [06.21.00 - LATEST] */
uint32 reset_counter = 10;
}
message ProtoUpdateInfo
{
/* Timestamp of the last update check in seconds since 1st of January 1970 (epoch time). *
* Status: In use [06.00 - LATEST] */
uint32 timestamp = 1;
/* Status of the last update check: *
* - 1 - No update check *
* - 2 - No error *
* - 3 - UDP socekt error *
* - 4 - Invalid hash *
* - 5 - Missing packet *
* - 6 - Invalid data *
* - 7 - Sending timeout *
* - 8 - No software to update *
* - 9 - Sending unexpected error *
* - 10 - Unexpected error *
* Status: In use [06.00 - LATEST] */
uint32 status = 2;
}
enum ModemType
{
/* Invalid value */
/* Invalid value. */
MODEM_TYPE_UNSPECIFIED = 0;
/* Quectel BC66 modem */
/* Quectel BC66 modem. */
MODEM_TYPE_BC66 = 1;
/* Quectel BC66-NA modem */
/* Quectel BC66-NA modem. */
MODEM_TYPE_BC66NA = 2;
/* Uses a shared modem from another sensor. */
MODEM_TYPE_SHARED_MODEM = 3;
/* Quectel BC660 modem. */
MODEM_TYPE_BC660 = 4;
}
message ProtoRuntime
enum ModemFirmwareVersion
{
/* Invalid value. */
MODEM_FIRMWARE_VERSION_UNSPECIFIED = 0;
/* Up-time in seconds (since reset) */
uint32 up_time = 1;
/* Unable to read firmware version from device. */
MODEM_FIRMWARE_VERSION_READING_ERROR = 1;
/* Message counters (since reset). There are 3 counters: */
/* message_counters[0] - Counter of confirmable messages attempts */
/* message_counters[1] - Counter of non-confirmable messages attempts */
/* message_counters[2] - Counter of succeeded messages */
repeated uint32 message_counters = 2;
/* Unknown firmware version. */
MODEM_FIRMWARE_VERSION_UNKNOWN = 2;
/* MCU temperature in Celsius */
sint32 mcu_temperature = 3;
/* BC660KGLAAR01A05_01.002.01.002. */
MODEM_FIRMWARE_VERSION_BC660_V1 = 3;
/* Minimum battery voltage in mV */
uint32 min_battery_voltage = 4;
/* BC660KGLAAR01A05_01.200.01.200. */
MODEM_FIRMWARE_VERSION_BC660_V2 = 4;
/* MCU temperature in Celsius, while the minimum battery voltage was reached */
sint32 min_battery_mcu_temperature = 5;
/* BC660KGLAAR01A05_01.202.01.202. */
MODEM_FIRMWARE_VERSION_BC660_V3 = 5;
/* Battery reset timestamp (Unix timestamp) */
uint32 battery_reset_timestamp = 6;
/* BC660KGLAAR01A05_01.203.01.203. */
MODEM_FIRMWARE_VERSION_BC660_V4 = 6;
/* Max MCU temperature in Celsius */
sint32 max_mcu_temperature = 7;
/* BC660KGLAAR01A05_01.204.01.204. */
MODEM_FIRMWARE_VERSION_BC660_V5 = 7;
/* Min MCU temperature in Celsius */
sint32 min_mcu_temperature = 8;
/* BC660KGLAAR01A05_01.205.01.205. */
MODEM_FIRMWARE_VERSION_BC660_V6 = 8;
/* Table of runtime errors. Max length: 20 */
repeated uint32 runtime_errors = 9;
/* BC660KGLAAR01A05_01.301.01.301. */
MODEM_FIRMWARE_VERSION_BC660_V7 = 9;
/* BC660KGLAAR01A05_01.303.01.303. */
MODEM_FIRMWARE_VERSION_BC660_V8 = 10;
}
message ProtoModem
{
/* Modem type. *
* Status: In use [06.00 - LATEST] */
ModemType type = 1;
/* Parameters for BC66 modem: */
/* parameters[0] - sc_EARFCN - Range: [0:262143]. Unknown value: -1 */
/* parameters[1] - sc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */
/* parameters[2] - sc_PCI - Range: [0:502]. Unknown value: -1 */
/* parameters[3] - sc_Cell id - Range: [1:268435456]. Unknown value: 0 */
/* parameters[4] - sc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */
/* parameters[5] - sc_RSRQ - [dB] - Range: [-20:-3]. Unknown value: 0 */
/* parameters[6] - sc_RSSI - [dBm] - Range: [-110:-3] Unknown value: 0 */
/* parameters[7] - sc_SINR - [dB] - Range: [-10:30]. Unknown value: 31 */
/* parameters[8] - sc_Band - Range: [see module supported bands]. The current serving cell band. Unknown value: -1 */
/* parameters[9] - sc_TAC - Range: [0:65536]. Unknown value: -1 */
/* parameters[10] - sc_ECL - Range: [0:2]. Unknown value: -1 */
/* parameters[11] - sc_TX_PWR - [0.1cBm] - Range [-440:230]. Unknown value: -1000 */
/* parameters[12] - OP_MODE - Range: [0:3]. Unknown value: -1 */
/* parameters[13] - nc_EARFCN - Range: [0:262143]. Unknown value: -1 */
/* parameters[14] - nc_EARNFCN_offset - Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 */
/* parameters[15] - nc_PCI - Range: [0:502]. Unknown value: -1 */
/* parameters[16] - nc_RSRP - [dBm] - Range: [-140:-44]. Unknown value: 0 */
/* parameters[17] - RLC_UL_BLER - Range: [0:100]. Unknown value: -1 */
/* parameters[18] - RLC_DL_BLER - Range: [0:100]. Unknown value: -1 */
/* parameters[19] - MAC_UL_BLER - Range: [0:100]. Unknown value: -1 */
/* parameters[20] - MAC_DL_BLER - Range: [0:100]. Unknown value: -1 */
/* parameters[21] - MAC_UL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[22] - MAC_DL_TOTAL_BYTES - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[23] - MAC_UL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[24] - MAC_DL_total_HARQ_Tx - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[25] - MAC_UL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[26] - MAC_DL_HARQ_re_Tx - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[27] - RLC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[28] - RLC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[29] - MAC_UL_tput - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[30] - MAC_DL_tput - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[31] - sleep_duration - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
/* Modem runtime parameters. *
* For BC66 modem (34 parameters): *
* - 1st item - sc_EARFCN. Range: [0:262143]. Unknown value: -1 *
* - 2nd item - sc_EARNFCN_offset. Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 *
* - 3rd item - sc_PCI. Range: [0:502]. Unknown value: -1 *
* - 4th item - sc_Cell_Id. Range: [1:268435456]. Unknown value: 0 *
* - 5th item - sc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 6th item - sc_RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
* - 7th item - sc_RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
* - 8th item - sc_SINR [dB]. Range: [-10:30]. Unknown value: 31 *
* - 9th item - sc_Band. Range: [see module supported bands]. The current serving cell band. Unknown value: -1 *
* - 10th item - sc_TAC. Range: [0:65536]. Unknown value: -1 *
* - 11th item - sc_ECL. Range: [0:2]. Unknown value: -1 *
* - 12th item - sc_TX_PWR [0.1dBm]. Range [-440:230]. Unknown value: -1000 *
* - 13th item - OP_MODE. Range: [0:3]. Unknown value: -1 *
* - 14th item - nc_EARFCN. Range: [0:262143]. Unknown value: -1 *
* - 15th item - nc_EARNFCN_offset. Range: [0:4] mapped to [-2, -1, -0.5, 0, 1]. Unknown value: -1 *
* - 16th item - nc_PCI. Range: [0:502]. Unknown value: -1 *
* - 17th item - nc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 18th item - RLC_UL_BLER. Range: [0:100]. Unknown value: -1 *
* - 19th item - RLC_DL_BLER. Range: [0:100]. Unknown value: -1 *
* - 20th item - MAC_UL_BLER. Range: [0:100]. Unknown value: -1 *
* - 21th item - MAC_DL_BLER. Range: [0:100]. Unknown value: -1 *
* - 22th item - MAC_UL_TOTAL_BYTES. Range: [0:2147483647]. Unknown value: -1 *
* - 23th item - MAC_DL_TOTAL_BYTES. Range: [0:2147483647]. Unknown value: -1 *
* - 24th item - MAC_UL_total_HARQ_Tx. Range: [0:2147483647]. Unknown value: -1 *
* - 25th item - MAC_DL_total_HARQ_Tx. Range: [0:2147483647]. Unknown value: -1 *
* - 26th item - MAC_UL_HARQ_re_Tx. Range: [0:2147483647]. Unknown value: -1 *
* - 27th item - MAC_DL_HARQ_re_Tx. Range: [0:2147483647]. Unknown value: -1 *
* - 28th item - RLC_UL_tput. Range: [0:2147483647]. Unknown value: -1 *
* - 29th item - RLC_DL_tput. Range: [0:2147483647]. Unknown value: -1 *
* - 30th item - MAC_UL_tput. Range: [0:2147483647]. Unknown value: -1 *
* - 31th item - MAC_DL_tput. Range: [0:2147483647]. Unknown value: -1 *
* - 32th item - sleep_duration [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* - 33th item - Rx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* - 34th item - Tx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* For BC660 modem (22 parameters): *
* - 1st item - sc_EARFCN. Range: [0:262143]. Unknown value: -1 *
* - 2nd item - sc_EARNFCN_offset. Range: [0:21] mapped to *
* [Invalid, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, -0.5, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. *
* Unknown value: -1 *
* - 3rd item - sc_PCI. Range: [0:503]. Unknown value: -1 *
* - 4th item - sc_Cell_Id. Range: [1:268435456]. Unknown value: 0 *
* - 5th item - sc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 6th item - sc_RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
* - 7th item - sc_RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
* - 8th item - sc_SINR [dB]. Range: [-10:30]. Unknown value: 31 *
* - 9th item - sc_Band. Range: [see module supported bands]. The current serving cell band. Unknown value: -1 *
* - 10th item - sc_TAC. Range: [0:65536]. Unknown value: -1 *
* - 11th item - sc_ECL. Range: [0:2]. Unknown value: -1 *
* - 12th item - sc_TX_PWR [dBm]. Range [-45:23]. Unknown value: 128 *
* - 13th item - OP_MODE. Range: [0:3] mapped to [In-band same PCI, In-band different PCI, Guard band, Stand alone]. *
* Unknown value: -1 *
* - 14th item - nc_EARFCN. Range: [0:262143]. Unknown value: -1 *
* - 15th item - nc_PCI. Range: [0:503]. Unknown value: -1 *
* - 16th item - nc_RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 17th item - nc_RSRQ [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 18th item - sleep_duration [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* - 19th item - Rx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* - 20th item - Tx_time [0.1s]. Range: [0:2147483647]. Unknown value: -1 *
* - 21st item - PLMN_state. Range: [0:3] mapped to [No PLMN, Searching, Selected, Unknown]. Unknown value: 3 *
* - 22nd item - select_PLMN. Range: [100:999999]. Unknown value: -1 *
* For shared modem client (4 parameters): *
* - 1st item - RSRP [dBm]. Range: [-140:-44]. Unknown value: 0 *
* - 2nd item - RSRQ [dB]. Range: [-20:-3]. Unknown value: 0 *
* - 3rd item - RSSI [dBm]. Range: [-110:-3] Unknown value: 0 *
* - 4th item - SINR [dB]. Range: [-10:30]. Unknown value: 31 *
* Status: In use [06.00 - LATEST] */
repeated sint32 parameters = 2;
/* ICCID of inserted/soldered sim card. String up to 22 characters long. */
/* 0x7F if sim card is not detected, empty (not sent) if device does not have modem. */
/* This field is only sent by device */
/* Integrated Circuit Card Identifier (ICCID) of the inserted/soldered SIM card. *
* String up to 22 characters: *
* A string containing only the special character 0x7F (DEL) indicates that the SIM card is not detected. *
* Status: In use [06.07.04/06.09.09/06.10.10/06.11.09/06.12.06/06.13.00/06.21.00 - LATEST] */
string sim_card_identification = 3;
/* Modem firmware version. *
* Status: In use [06.20.05/06.21.00 - LATEST] */
ModemFirmwareVersion firmware_version = 4;
/* Modem identification: *
* - IMEI (International Mobile Equipment Identity) of the modem *
* - Serial number of the most recently used modem-sharing sensor *
* String up to 15 characters. *
* Status: In use [07.00.00 - LATEST] */
string modem_identification = 5;
/* Modem statistics. *
* 1st item - Number of transmissions since the last device information message. Undefined value: 0. *
* 2nd item - Time in seconds since the last device information message. Undefined value: 0. *
* 3rd item - Total time in the power saving mode, in seconds. Undefined value: 0. *
* 4th item - Total time in the active state in seconds. Undefined value: 0. *
* Status: In use [07.00.00 - LATEST] */
repeated uint32 modem_statistics = 6;
}
message ProtoUpdateInfo
message ProtoDeviceInfo
{
/* Timestamp of update (Unix timestamp) */
uint32 timestamp = 1;
/* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
/* Update status, possible values: */
/* - 1 - No update yet */
/* - 2 - No error */
/* - 3 - UDP socekt error */
/* - 4 - Hash error */
/* - 5 - Missing packet error */
/* - 6 - Invalid data error */
/* - 7 - Sending timeout error */
/* - 8 - No SW to update error */
/* - 9 - Sending unexpected error */
/* - 10 - Unexpected error */
uint32 status = 2;
}
reserved 2,4,5,6,7,8,9,10,11,12;
message ProtoDeviceInfo
{
/* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
/* Serial number of device */
bytes serial_num = 1;
/* Serial number of the device. *
* Length: 6 bytes. *
* Status: In use [07.00.00 - LATEST] / Previously as serial_num [06.00 - 06.xx.xx] */
bytes serial_number = 1;
/* Deprecated field */
reserved 2;
/* DEVICE INFORMATION FIELDS ------------------------------------------------------------------------------------------------ */
/* Software version e.g ver 06.10 -> 0x060A -> 1546 */
/* Software version (excluding LTS). *
* Example: 1546 -> 0x060A -> SW version 06.10 *
* Status: In use [06.00 - LATEST] */
uint32 sw_version = 3;
/* Deprecated fields */
reserved 4,5,6,7,8,9,10,11,12;
/* Software commit ID and LTS version. *
* String of 7 characters: *
* - Commit ID (examples: "fa02cd0" means the beginning of the commit ID "fa02cd0") [06.00 - 06.06.xx] *
* - LTS version and commit ID (examples: "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f") *
[06.07.00 - LATEST] *
* Status: In use [06.00 - LATEST] */
string commit_id = 15;
/* Structure with battery and temperature information */
/* Device runtime information. *
* Status: In use [06.00 - LATEST] */
ProtoRuntime runtime_info = 13;
/* Structure with modem specific runtime information */
/* Memory statistics: *
* 1st item - Status of the non-volatile storage: *
* - 0 - Non-volatile storage works properly *
* - 1 - Non-volatile storage has some corrupt packets. Memory is read-only *
* - 2 - Non-volatile storage is corrupted. Memory is unavailable *
* 2nd item - Timestamp of the end of collecting statistics in seconds since 1st of January 1970 (epoch time). *
Undefined value: 4294967295. *
* 3rd item - Capacity of the memory in bytes. *
* 4th item - Used space in bytes. *
* 5th item - Size of invalid (outdated) packets in bytes. *
* 6th item - Size of corrupt packets in bytes. *
* 7th item - Number of valid packets. *
* 8th item - Number of invalid (outdated) packets. *
* 9th item - Number of corrupt packets. *
* 10th item - Number of all samples for channel 1 (valid packets). *
* 11th item - Number of all samples for channel 2 (valid packets). *
* 12th item - Number of all samples for channel 3 (valid packets). *
* 13th item - Number of all samples for channel 4 (valid packets). *
* 14th item - Number of all samples for channel 5 (valid packets). *
* 15th item - Number of all samples for channel 6 (valid packets). *
* 16th item - Timestamp of the first binary measurement in seconds since 1st of January 1970 (epoch time). *
* Undefined value: 4294967295. *
* 17th item - Timestamp of the last binary measurement in seconds since 1st of January 1970 (epoch time). *
* Undefined value: 4294967295. *
* 18th item - Timestamp of the last binary measurement marked as sent, in seconds since 1st of January 1970 (epoch time). *
* Undefined value: 4294967295. *
* 19th item - Timestamp of the first continuous measurement in seconds since 1st of January 1970 (epoch time). *
* Undefined value: 4294967295. *
* 20th item - Timestamp of the last continuous measurement in seconds since 1st of January 1970 (epoch time). *
* Undefined value: 4294967295. *
* 21th item - Timestamp of the last continuous measurement marked as sent, in seconds since 1st of January 1970 (epoch time).*
* Undefined value: 4294967295. *
* 22th item - NvM write counter. *
* Status: In use [06.02 - LATEST] */
repeated uint32 memory_statistics = 17;
/* Last update check information. *
* Status: In use [06.08.00 - LATEST] */
ProtoUpdateInfo last_update_info = 18;
/* Modem information. *
* Only used if the sensor has a modem. *
* Status: In use [06.00 - LATEST] */
ProtoModem modem = 14;
/* String up to 7 bytes long. Software commit id e.g. "e0e8556" */
/* From version 06.07 the first two characters indicate the LTS version. */
/* For example: the value "0bdd23f" means LTS version 11 and the beginning of the commit ID "dd23f" */
string commit_id = 15;
/* SERVER COMMUNICATION FIELDS ---------------------------------------------------------------------------------------------- */
/* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */
/* Cloud token. *
* Can be empty, set to any user-defined value, or hold device IMEI. *
* String up to 36 characters. *
* This field is only sent during server communication. *
* Status: In use [06.00 - LATEST] */
string cloud_token = 16;
/* Memory statistics: */
/* memory_statistics[0] - Status of Nv storage: */
/* - 0 - Nv storage hasn't errors */
/* - 1 - Nv storage has some corrupted packet. Memory is read-only */
/* - 2 - Nv storage is corrupted. Memory is unavailable */
/* memory_statistics[1] - Timestamp of the end of collecting statistics. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[2] - Capacity of memory in bytes */
/* memory_statistics[3] - Used space in bytes */
/* memory_statistics[4] - Size of invalid (outdated) packets in bytes */
/* memory_statistics[5] - Size of corrupted packets in bytes */
/* memory_statistics[6] - Number of valid packets */
/* memory_statistics[7] - Number of invalid (outdated) packets */
/* memory_statistics[8] - Number of corrupted packets */
/* memory_statistics[9] - Number of all samples for channel 1 (valid packets) */
/* memory_statistics[10] - Number of all samples for channel 2 (valid packets) */
/* memory_statistics[11] - Number of all samples for channel 3 (valid packets) */
/* memory_statistics[12] - Number of all samples for channel 4 (valid packets) */
/* memory_statistics[13] - Number of all samples for channel 5 (valid packets) */
/* memory_statistics[14] - Number of all samples for channel 6 (valid packets) */
/* memory_statistics[15] - Timestamp of the first binary measurement. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[16] - Timestamp of the last binary measurement. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[17] - Timestamp of the last binary measurement, that marked as sent. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[18] - Timestamp of the first continuous measurement. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[19] - Timestamp of the last continuous measurement. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[20] - Timestamp of the last continuous measurement, that marked as sent. */
/* Value in seconds since UNIX EPOCH 01-01-1970. Undefined value: 4294967295 */
/* memory_statistics[21] - NVM write counter */
repeated uint32 memory_statistics = 17;
/* Information about last sensor SW update */
ProtoUpdateInfo last_update_info = 18;
/* Enclosure tamper alert: *
* - 0 - Inactive *
* - 1 - Active (enclosure opened) *
* - 2 - Inactive, was active (enclosure closed) *
* The transition from 'Inactive, was active' to 'Inactive' occurs after the alert has been successfully sent to the server *
* or the user has reset the alert via Bluetooth. *
* This field is only sent during server communication. *
* Status: In use [07.00.01 - LATEST] */
uint32 enclosure_tamper_alert = 19;
}

185
common/transport/coap/src/main/proto/efento/proto_measurement_types.proto

@ -19,160 +19,201 @@ option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "MeasurementTypeProtos";
enum MeasurementType {
/* [] - No sensor on the channel */
/* [] - No sensor on the channel. */
MEASUREMENT_TYPE_NO_SENSOR = 0;
/* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. Type: Continuous */
/* [°C] - Celsius degree. Temperature. Resolution: 0.1°C. Range: [-273.2:4000.0]. Type: Continuous. */
MEASUREMENT_TYPE_TEMPERATURE = 1;
/* [% RH] - Relative humidity. Resolution 1%. Range [0:100]. Type: Continuous */
/* [% RH] - Percentage. Relative humidity. Resolution: 1%. Range: [0:100]. Type: Continuous. */
MEASUREMENT_TYPE_HUMIDITY = 2;
/* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. Atmospheric pressure. Type: Continuous */
/* [hPa] - Hectopascal. Atmospheric pressure. Resolution: 0.1hPa. Range: [1.0:2000.0]. Type: Continuous. */
MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE = 3;
/* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */
/* [Pa] - Pascal. Differential pressure. Resolution: 1Pa. Range: [-10000:10000]. Type: Continuous. */
MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4;
/* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
/* Sign indicates state: (+) Alarm, (-) OK. Type: Binary. */
MEASUREMENT_TYPE_OK_ALARM = 5;
/* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
/* [IAQ] - IAQ index. Air quality. Resolution: 1IAQ. Range: [0:500]. Type: Continuous. *
* To obtain the IAQ index, the measurement value should be divided by 3. *
* Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
* - 0 - Calibration required (sensor returns not accurate values) *
* - 1 - Calibration on-going (sensor returns not accurate values) *
* - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_IAQ = 6;
/* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
/* Sign indicates state: (+) Water detected, (-) Water not detected. Type: Binary. */
MEASUREMENT_TYPE_FLOODING = 7;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */
/* [NB] - Number of pulses. Number of pulses in a single period. Resolution: 1 pulse. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_PULSE_CNT = 8;
/* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */
/* [Wh] - Watt-hour. Number of watt-hours in a single period. Resolution: 1Wh. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_ELECTRICITY_METER = 9;
/* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */
/* [l] - Liter. Number of litres in a single period. Resolution: 1l. Range: [0:8000000]. Type: Continuous. */
MEASUREMENT_TYPE_WATER_METER = 10;
/* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */
/* [kPa] - Kilopascal. Soil moisture (tension). Resolution: 1kPa. Range: [-1000:0]. Type: Continuous. */
MEASUREMENT_TYPE_SOIL_MOISTURE = 11;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */
/* [ppm] - Parts per million. Carbon monoxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_CO_GAS = 12;
/* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */
/* [ppm] - Parts per million. Nitrogen dioxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_NO2_GAS = 13;
/* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */
/* [ppm] - Parts per million. Hydrogen sulfide concentration. Resolution: 0.01ppm. Range: [0.00:80000.00]. Type: Continuous. */
MEASUREMENT_TYPE_H2S_GAS = 14;
/* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */
/* [lx] - Lux. Illuminance. Resolution: 0.1lx. Range: [0.0:100000.0]. Type: Continuous. */
MEASUREMENT_TYPE_AMBIENT_LIGHT = 15;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
/* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 1 micrometers. *
* Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_1_0 = 16;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
/* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 2.5 micrometers. *
* Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_2_5 = 17;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
/* [µg/m^3] - Microgram per cubic meter. Particles with an aerodynamic diameter of less than 10 micrometers. *
* Resolution: 1µg/m^3. Range: [0:1000]. Type: Continuous. */
MEASUREMENT_TYPE_PM_10_0 = 18;
/* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */
/* [dB] - Decibel. Noise level. Resolution: 0.1 dB. Range: [0.0:200.0]. Type: Continuous. */
MEASUREMENT_TYPE_NOISE_LEVEL = 19;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */
/* [ppm] - Parts per million. Ammonia concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_NH3_GAS = 20;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */
/* [ppm] - Parts per million. Methane concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_CH4_GAS = 21;
/* [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. Type: Continuous */
/* [kPa] - Kilopascal (100kPa = 1bar). Pressure. Resolution: 1kPa. Range: [0:200000]. Type: Continuous. */
MEASUREMENT_TYPE_HIGH_PRESSURE = 22;
/* [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. Type: Continuous */
/* [mm] - Millimeter. Distance. Resolution: 1mm. Range: [0:100000]. Type: Continuous. */
MEASUREMENT_TYPE_DISTANCE_MM = 23;
/* [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). Type: Continuous */
/* [l] - Liter. Water meter (minor). Resolution: 1l. Range: [0:99]. Type: Continuous. *
* To obtain the liters, the measurement value should be divided by 6. *
* Measurement also includes a major channel index related with the minor channel as metadata *
* (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_WATER_METER_ACC_MINOR = 24;
/* [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). Type: Continuous */
/* [hl] - Hectoliter. Water meter (major). Resolution: 1hl. Range: [0:999999]. Type: Continuous. *
* To obtain the hectoliters, the measurement value should be divided by 4. *
* Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
* - 0 - Counter works properly *
* - 1 - Error occurred (the counted value may be underestimated) *
* - 2 - Sensor reset occurred (the counted value may be underestimated) *
* - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR = 25;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. Type: Continuous */
/* [ppm] - Parts per million. Carbon dioxide concentration. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. *
* To obtain the carbon dioxide concentration, the measurement value should be divided by 3. *
* Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
* - 0 - Auto calibration has not yet been performed *
* - 1 - The last auto calibration was successful *
* - 2 - Last auto calibration failed */
MEASUREMENT_TYPE_CO2_GAS = 26;
/* [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. Type: Continuous */
/* [% RH] - Percentage. Relative humidity (accurate). Resolution: 0.1%. Range: [0.0:100.0]. Type: Continuous. */
MEASUREMENT_TYPE_HUMIDITY_ACCURATE = 27;
/* [sIAQ] - Static IAQ index. Resolution 1IAQ. Range [0:10000]. To get static IAQ index the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
/* [sIAQ] - Static IAQ index. Air quality. Resolution: 1sIAQ. Range: [0:10000]. Type: Continuous. *
* To obtain the static IAQ index, the measurement value should be divided by 3. *
* Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
* - 0 - Calibration required (sensor returns not accurate values) *
* - 1 - Calibration on-going (sensor returns not accurate values) *
* - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_STATIC_IAQ = 28;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. CO2 equivalent. */
/* To get CO2 equivalent the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
/* [ppm] - Parts per million. CO2 equivalent. Resolution: 1ppm. Range: [0:1000000]. Type: Continuous. *
* To obtain the CO2 equivalent, the measurement value should be divided by 3. *
* Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
* - 0 - Calibration required (sensor returns not accurate values) *
* - 1 - Calibration on-going (sensor returns not accurate values) *
* - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_CO2_EQUIVALENT = 29;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
/* To get breath VOC estimate the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
/* [ppm] - Parts per million. Breath VOC estimate. Resolution: 1ppm. Range: [0:100000]. Type: Continuous. *
* To obtain the breath VOC estimate, the measurement value should be divided by 3. *
* Measurement also includes calibration status as metadata (is the remainder of the absolute value divided by 3): *
* - 0 - Calibration required (sensor returns not accurate values) *
* - 1 - Calibration on-going (sensor returns not accurate values) *
* - 2 - Calibration done (best accuracy of IAQ sensor) */
MEASUREMENT_TYPE_BREATH_VOC = 30;
/* Special measurement type reserved for cellular gateway. */
/* Type: Continuous */
MEASUREMENT_TYPE_CELLULAR_GATEWAY = 31;
/* Reserved for the Shared Modem functionality. Type: Continuous. */
MEASUREMENT_TYPE_SHARED_MODEM = 31;
/* [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. Type: Continuous */
/* [%] - Percentage. Generic type. Resolution: 0.01%. Range: [0.00:100.00]. Type: Continuous. */
MEASUREMENT_TYPE_PERCENTAGE = 32;
/* [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. Type: Continuous */
/* [mV] - Millivolt. Voltage. Resolution: 0.1mV. Range: [0.0:100000.0]. Type: Continuous. */
MEASUREMENT_TYPE_VOLTAGE = 33;
/* [mA] - Milliampere. Resolution 0.01mA. Range [0.0:10000.00]. Type: Continuous */
/* [mA] - Milliampere. Current. Resolution: 0.01mA. Range: [0.00:10000.00]. Type: Continuous. */
MEASUREMENT_TYPE_CURRENT = 34;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0:1000000]. Type: Continuous */
/* [NB] - Number of pulses. Pulse counter (minor). Resolution: 1 pulse. Range: [0:999]. Type: Continuous. *
* To obtain the number of pulses, the measurement value should be divided by 6. *
* Measurement also includes a major channel index related with the minor channel as metadata *
* (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR = 35;
/* [kNB] Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. Type: Continuous */
/* [kNB] - Number of kilopulses. Pulse counter (major). Resolution: 1 kilopulse. Range: [0:999999]. Type: Continuous. *
* To obtain the number of kilopulses, the measurement value should be divided by 4. *
* Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
* - 0 - Counter works properly *
* - 1 - Error occurred (the counted value may be underestimated) *
* - 2 - Sensor reset occurred (the counted value may be underestimated) *
* - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR = 36;
/* [Wh] - Watt-hour; Resolution 1Wh. Range [0:1000000]. Number of watt-hours in a single period. Type: Continuous */
/* [Wh] - Watt-hour. Electricity meter (minor). Resolution: 1Wh. Range: [0:999]. Type: Continuous. *
* To obtain the watt-hours, the measurement value should be divided by 6. *
* Measurement also includes a major channel index related with the minor channel as metadata *
* (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR = 37;
/* [kWh] - Kilowatt-hour; Resolution 1kWh. Range [0:1000000]. Number of kilowatt-hours in a single period. Type: Continuous */
/* [kWh] - Kilowatt-hour. Electricity meter (major). Resolution: 1kWh. Range: [0:999999]. Type: Continuous. *
* To obtain the kilowatt-hours, the measurement value should be divided by 4. *
* Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
* - 0 - Counter works properly *
* - 1 - Error occurred (the counted value may be underestimated) *
* - 2 - Sensor reset occurred (the counted value may be underestimated) *
* - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR = 38;
/* [NB] Number of pulses (wide range). Resolution 1 pulse. Range [0:999999]. Type: Continuous */
/* [NB] - Number of pulses. Wide-range pulse counter (minor). Resolution: 1 pulse. Range: [0:999999]. Type: Continuous. *
* To obtain the number of pulses, the measurement value should be divided by 6. *
* Measurement also includes a major channel index related with the minor channel as metadata *
* (is the remainder of the absolute value divided by 6). */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR = 39;
/* [MNB] Number of megapulses (wide range). Resolution 1 megapulse. Range [0:999999]. Type: Continuous */
/* [MNB] - Number of megapulses. Wide-range pulse counter (major). Resolution: 1 megapulse. Range: [0:999999]. *
* Type: Continuous. *
* To obtain the number of megapulses, the measurement value should be divided by 4. *
* Measurement also includes a measurement status as metadata (is the remainder of the absolute value divided by 4): *
* - 0 - Counter works properly *
* - 1 - Error occurred (the counted value may be underestimated) *
* - 2 - Sensor reset occurred (the counted value may be underestimated) *
* - 3 - Sensor reset and error occurred (the counted value may be underestimated) */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR = 40;
/* [mA] - Milliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. Type: Continuous */
/* [mA] - Milliampere. Current (precise). Resolution: 0.001mA. Range: [-4000.000:4000.000]. Type: Continuous. */
MEASUREMENT_TYPE_CURRENT_PRECISE = 41;
/* Sign indicates state: (+) ON, (-) OFF. Type: Binary */
/* Sign indicates state: (+) ON, (-) OFF. Type: Binary. */
MEASUREMENT_TYPE_OUTPUT_CONTROL = 42;
}
/* [Ω] - Ohm. Resolution: 1Ω. Range: [0:1000000]. Type: Continuous. */
MEASUREMENT_TYPE_RESISTANCE = 43;
}

217
common/transport/coap/src/main/proto/efento/proto_measurements.proto

@ -14,6 +14,7 @@
* limitations under the License.
*/
syntax = "proto3";
import "efento/proto_measurement_types.proto";
option java_package = "org.thingsboard.server.gen.transport.coap";
@ -21,106 +22,166 @@ option java_outer_classname = "MeasurementsProtos";
message ProtoChannel {
/* Type of channel */
/* Reserved fields. */
reserved 6,7,8;
/* Type of measurement. *
* Status: In use [06.00 - LATEST] */
MeasurementType type = 1;
/* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */
/* Timestamp of the first (oldest) sample in seconds since 1st of January 1970 (epoch time). *
* Status: In use [06.00 - LATEST] */
int32 timestamp = 2;
/* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */
/* measurements in the package. */
/* Format defined by 'MeasurementType' field */
/* Start point of measurement values. *
* This is used as the base value for calculating the values of all channel measurements in the message. *
* Used for 'Continuous' sensor types only. *
* Format is defined by 'MeasurementType' field. *
* Status: In use [06.00 - LATEST] */
sint32 start_point = 4;
/* 'Continuous' sensor types */
/* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */
/* 'sample_offsets' format defined by 'MeasurementType' field. */
/* If the 'sample_offset' has a value from the range [8355840: 8388607], it should be interpreted as a sensor error code. */
/* In that case value of the 'start_point' field should not be added to this 'sample_offset'. See ES6-264 for error codes. */
/* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20, */
/* sample_offset[2] = 8388605 */
/* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */
/* 3rd sample in the package has no temperature value. It has information about failure of MCP9808 (temperature) sensor. */
/* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */
/* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760, */
/* timestamp of the 3rd sample 1606391820 */
/* 'Binary' sensor types: */
/* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */
/* Sign (- or +) indicates the state of measurements depending on the sensor type. */
/* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */
/* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + absolute value -1. */
/* Values of this field are incremented starting from 1 (1->0: state at the time */
/* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */
/* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */
/* measurements. Only state changes in the time range are included in the 'sample_offsets' field */
/* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */
/* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */
/* Measurement value offsets. *
* For 'Continuous' sensor types: *
* These are the values of the offsets from the 'start_point' for each channel measurement in the message. *
* Format is defined by 'MeasurementType' field. The first sample is the oldest. *
* If the sample offset has a value in the range [8355840:8388607], it should be interpreted as a sensor error code. *
* In this case, the value of the 'start_point' field should not be added to this sample offset. *
* Example: type = 1 (temperature) *
* start_point = 100 *
* sample_offsets[0] = 15, sample_offsets[1] = 20, sample_offsets[2] = 8388557 *
* timestamp = 1606391700 *
* measurement_period_base = 60 *
* measurement_period_factor = 1 *
* 1st measurement is the temperature value = 11.5°C, measured at 1606391700 *
* 2nd measurement is the temperature value = 12°C, measured at 1606391760 *
* 3rd measurement is the error code (failure of the SHT4x temperature sensor), measured at 1606391820 *
* For 'Binary' sensor types: *
* The absolute value of the sample offset minus 1 is the offset in seconds from the 'timestamp' field. *
* Sign (- or +) indicates the state of the measurement depending on the sensor type. *
* The first and the last samples define the time range of the measurements. *
* Only state changes within the time range are included in the 'sample_offsets' field. *
* Example: type = 5 (OK/Alarm) *
* sample_offsets[0] = 1, sample_offsets[1] = -9, sample_offsets[2] = 13 *
* timestamp = 1606391700 *
* 1st measurement is the state at 1606391700, which is Alarm (+) *
* 2nd measurement is the state at 1606391708, which is OK (-) *
* 3rd measurement is the state at 1606391712, which is Alarm (+) *
* Status: In use [06.00 - LATEST] */
repeated sint32 sample_offsets = 5 [packed=true];
/* Deprecated - configuration is sent to endpoint 'c' */
/* int32 lo_threshold = 6; */
reserved 6;
/* Deprecated - configuration is sent to endpoint 'c' */
/* int32 hi_threshold = 7; */
reserved 7;
/* Deprecated - configurations sent to endpoint 'c' */
/* int32 diff_threshold = 8; */
reserved 8;
}
message ProtoMeasurements {
/* Serial number of the device */
bytes serial_num = 1;
/* Battery status: true - battery ok, false - battery low */
bool battery_status = 2;
/* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */
/* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
/* Sensors of 'Binary' type take measurement each Measurement_period_base. */
/* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */
/* not sent (equal to 0), then the default value '14' shall be used for period calculation. */
/* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */
/* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
/* measurement period base in seconds */
/* RESERVED FIELDS ---------------------------------------------------------------------------------------------------------- */
reserved 10,11,12,13,14,15;
/* DEVICE STATUS FIELDS ----------------------------------------------------------------------------------------------------- */
/* Serial number of the device. *
* Length: 6 bytes. *
* Status: In use [07.00.00 - LATEST] / Previously as serial_num [06.00 - 06.xx.xx] */
bytes serial_number = 1;
/* Identifier of the current configuration. *
* The value of this field changes with every configuration change. *
* Status: In use [07.00.00 - LATEST] / Previously as hash [06.00 - 06.xx.xx] */
uint32 configuration_hash = 9;
/* Identifier of the current extended configuration. *
* The value of this field changes with every extended configuration change. *
* Status: In use [07.00.00 - LATEST] */
uint32 extended_configuration_hash = 17;
/* Battery level and status: *
* - [1:65525] - Battery voltage measurement and a battery status *
* - [65526:65531] - No battery voltage measurement and a battery status *
* To obtain the battery voltage, the battery_level value should be divided by 6 and the remainder of the division discarded. *
* The result is in hundredths of a volt or 10921 (no battery voltage measurement). *
* Battery level also includes a battery status as metadata (the remainder of the value divided by 6): *
* - 0 - Device is powered by a battery *
* - 1 - Device is powered by a rechargeable battery *
* - 2 - Device is powered by an external source and a battery is charging *
* - 3 - Reserved for the future use *
* - 4 - Reserved for the future use *
* - 5 - Reserved for the future use *
* Example: battery_level = 2474 *
* Battery voltage = 2474/6 = 412 = 4.12[V] *
* Battery status = 2474 % 6 = 2 *
* Status: In use [07.00.00 - LATEST] */
uint32 battery_level = 18;
/* MEASUREMENT FIELDS ------------------------------------------------------------------------------------------------------- */
/* Measurement period defines how often the measurements are to be taken. *
* Sensors of 'Continuous' type take measurement each 'measurement_period_base' * 'measurement_period_factor'. *
* Sensors of 'Binary' type take measurement each 'measurement_period_base'. *
* For backward compatibility with versions 5.xx in case of 'Binary/Mixed' sensors, if the 'measurement_period_factor' is *
* not sent (equal to 0), then the default value '14' shall be used for the period calculation. *
* For backward compatibility with versions 5.xx in case of 'Continuous' sensors, if the 'measurement_period_factor' is *
* not sent (equal to 0), then the default value '1' shall be used for the period calculation. */
/* Measurement period base in seconds. *
* Status: In use [06.00 - LATEST] */
uint32 measurement_period_base = 3;
/* Measurement period factor */
/* Measurement period factor. *
* Status: In use [06.00 - LATEST] */
uint32 measurement_period_factor = 8;
/* Measurements grouped by channel. *
* Status: In use [06.00 - LATEST] */
repeated ProtoChannel channels = 4;
/* Timestamp of the next scheduled transmission. If the device will not send data until this time, */
/* it should be considered as 'lost' */
/* SERVER COMMUNICATION FIELDS ---------------------------------------------------------------------------------------------- */
/* Battery status: *
* - True - Battery OK *
* - False - Battery discharged *
* Status: In use [06.00 - LATEST] */
bool battery_status = 2;
/* Timestamp of the next scheduled transmission. *
* If the device has not sent any data by this time, it should be considered 'lost'. *
* This field is only sent during server communication. *
* Status: In use [06.00 - LATEST] */
uint32 next_transmission_at = 5;
/* Reason of transmission - unsigned integer where each bit indicates different possible communication reason. */
/* Can be more than one: */
/* - bit 0: first message after sensor reset */
/* - bit 1: user button triggered */
/* - bit 2: user BLE triggered */
/* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 31. */
/* Set to 0 after a successful transmission. */
/* - bit 8...19: rule 1...12 was met */
/* - bit 20: triggered after the end of the limit */
/* Reason for transmission. *
* Bitmask, where each bit represents a specific reason for initiating communication (multiple reasons can be active *
* simultaneously): *
* - Bit 0 - First message after sensor reset *
* - Bit 1 - User-triggered (button press) *
* - Bit 2 - User-triggered (Bluetooth) *
* - Bit 3:7 - Number of retries - incremented after each failed transmission and reset upon success. Range: [0:31] *
* - Bit 8:19 - Rule-based triggers (12 bits): *
* - [06.00 - 07.01.xx]: Bitmask indicating which of the rules 1-12 were met *
* - [07.02.00 - LATEST]: The transfer bit 'i' (where i = 0 to 11) is set if either rule 'i' (0-11) is set *
* or if rule 'i + 12' (12-15) is set. This aggregation applies only to bits 0-3 (rules 0-3 and 12-15). *
* The mask aggregates 16 rules (0-15) onto 12 bits. *
* - Bit 20 - Triggered at the end of the defined limit *
* - Bit 21 - Triggered after a PIN tamper alert *
* - Bit 22 - Triggered after an enclosure tamper alert *
* - Bit 23 - Triggered before shutdown *
* This field is only sent during server communication. *
* Status: In use [06.00 - LATEST] */
uint32 transfer_reason = 6;
/* Signal strength level mapped from RSSI: */
/* - 0: RSSI < -110 dBm */
/* - 1: -110 dBm <= RSSI < -109 dBm */
/* - 2...61: -109 <= RSSI < -108 dBm ... -50 dBm <= RSSI < -49 dBm */
/* - 62: -49 dBm <= RSSI < -48 dBm */
/* - 63: RSSI >= -48 dBm */
/* - 99: Not known or not detectable */
/* Signal strength (RSSI - Received Signal Strength Level): *
* - 0 - RSSI < -110dBm *
* - 1 - -110dBm <= RSSI < -109dBm *
* - 2:61 - -109dBm <= RSSI < -108dBm ... -50dBm <= RSSI < -49dBm *
* - 62 - -49dBm <= RSSI < -48dBm *
* - 63 - RSSI >= -48dBm *
* - 99 - Unknown or undetectable *
* This field is only sent during server communication. *
* Status: Deprecated [06.00 - 06.xx.xx] */
uint32 signal = 7;
/* Hash of the current configuration. Hash value changes each time a device receives a new configuration */
uint32 hash = 9;
/* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */
/* Cloud token. *
* Can be empty, set to any user-defined value, or hold device IMEI. *
* String up to 36 characters. *
* This field is only sent during server communication. *
* Status: In use [06.00 - LATEST] */
string cloud_token = 16;
}

407
common/transport/coap/src/main/proto/efento/proto_rule.proto

@ -18,270 +18,287 @@ syntax = "proto3";
option java_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "ProtoRuleProtos";
/* Encoding A: used to set absolute values in the Rules (e.g. upper and lower threshold values) */
/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. */
/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [0:100]. */
/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. */
/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000] */
/* - OK/ALARM - Not applicable */
/* - IAQ - [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. */
/* - FLOODING - Not applicable */
/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. */
/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [0:8000000]. Average power consumption in period */
/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [0:8000000]. Average water flow in period. */
/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). */
/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. */
/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Nitrogen dioxide concentration. */
/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. */
/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. */
/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0:1000]. */
/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. */
/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. */
/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. */
/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. */
/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. */
/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). */
/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). */
/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. */
/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. */
/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [0:10000]. */
/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */
/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */
/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. */
/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.00:10000.00]. */
/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */
/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */
/* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */
/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */
/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:999999]. */
/* Accumulative pulse counter wide range (minor). */
/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [0:999999]. */
/* Accumulative pulse counter wide range (major). */
/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. */
/* - OUTPUT_CONTROL - Not applicable */
/* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */
/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */
/* - HUMIDITY - [% RH] - Relative humidity. Resolution 1%. Range [1:100]. */
/* - ATMOSPHERIC_PRESSURE - [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [0.1:1999.0]. */
/* - DIFERENTIAL_PRESSURE - [Pa] - Pascal. Resolution 1Pa. Range [1:20000] */
/* - OK/ALARM - Not applicable */
/* - VOC - [IAQ] - Iaq index. Resolution 1IAQ. Range [1:500]. */
/* - FLOODING - Not applicable */
/* - PULSE_CNT - [NB] Number of pulses. Resolution 1 pulse. Range [1:8000000]. */
/* - ELECTRICITY_METER - [W] - Watt; Resolution 1W. Range [1:8000000]. Average power consumption in period */
/* - WATER_METER [l/min] - Liter per minute. Resolution 1l/min. Range [1:8000000]. Average water flow in period. */
/* - SOIL_MOISTURE - [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [1:1000]. Soil moisture (tension). */
/* - CO_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon monoxide concentration. */
/* - NO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Nitrogen dioxide concentration. */
/* - H2S_GAS - [ppm] - Parts per million. Resolution 0.01ppm. Range [0.01:80000.00]. Hydrogen sulfide concentration. */
/* - AMBIENT_LIGHT -[lx] - Lux. Resolution 0.1lx. Range [0.1:100000.0]. Illuminance. */
/* - PM_1_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
/* - PM_2_5 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
/* - PM_10_0 - [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [1:1000]. */
/* - NOISE_LEVEL - [dB] - Decibels. Resolution 0.1 dB. Range: [0.1:200.0]. Noise level. */
/* - NH3_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Ammonia concentration. */
/* - CH4_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Methane concentration. */
/* - HIGH_PRESSURE - [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [1:200000]. Pressure. */
/* - DISTANCE_MM - [mm] - Millimeter. Resolution 1mm. Range [1:100000]. Distance. */
/* - WATER_METER_ACC_MINOR - [l] - Liter. Resolution 1l. Range [1:1000000]. Accumulative water meter (minor). */
/* - WATER_METER_ACC_MAJOR - [hl] - Hectoliter. Resolution 1hl. Range [1:1000000]. Accumulative water meter (major). */
/* - CO2_GAS - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide concentration. */
/* - HUMIDITY ACCURATE - [% RH] - Relative humidity. Resolution 0.1%. Range [0.1:100.0]. */
/* - STATIC_IAQ - [sIAQ] - Static IAQ index. Resolution 1sIAQ. Range [1:10000]. */
/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */
/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */
/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.1:100000.0]. */
/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.01:10000.00]. */
/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */
/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */
/* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */
/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */
/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:999999]. */
/* Accumulative pulse counter wide range (minor). */
/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [1:999999]. */
/* Accumulative pulse counter wide range (major). */
/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [0.001:8 000.000]. */
/* - OUTPUT_CONTROL - Not applicable */
/* Condition to be checked by the device. If the condition is true, an action is triggered */
/* Encoding A: used to set absolute values in rules (e.g., upper and lower thresholds): *
* - TEMPERATURE - [°C] Celsius degree. Resolution: 0.1°C. Range: [-273.2:4000.0] *
* - HUMIDITY - [% RH] Percentage (Relative humidity). Resolution: 1%. Range: [0:100] *
* - ATMOSPHERIC_PRESSURE - [hPa] Hectopascal. Resolution: 0.1hPa. Range: [1.0:2000.0] *
* - DIFERENTIAL_PRESSURE - [Pa] Pascal. Resolution: 1Pa. Range: [-10000:10000] *
* - OK_ALARM - Not applicable *
* - IAQ - [IAQ] IAQ index. Resolution: 1IAQ. Range: [0:500] *
* - FLOODING - Not applicable *
* - PULSE_CNT - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:8000000]. Period number of pulses *
* - ELECTRICITY_METER - [W] Watt. Resolution: 1W. Range: [0:8000000]. Period average power consumption *
* - WATER_METER - [l/min] Liter per minute. Resolution: 1l/min. Range: [0:8000000]. Period average water *
* - SOIL_MOISTURE - [kPa] Kilopascal. Resolution: 1kPa. Range: [-1000:0] *
* - CO_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - NO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - H2S_GAS - [ppm] Parts per million. Resolution: 0.01ppm. Range: [0.00:80000.00] *
* - AMBIENT_LIGHT - [lx] Lux. Resolution: 0.1lx. Range: [0.0:100000.0] *
* - PM_1_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
* - PM_2_5 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
* - PM_10_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [0:1000] *
* - NOISE_LEVEL - [dB] Decibel. Resolution: 0.1 dB. Range: [0.0:200.0] *
* - NH3_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - CH4_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - HIGH_PRESSURE - [kPa] Kilopascal (100kPa = 1bar). Resolution: 1kPa. Range: [0:200000] *
* - DISTANCE_MM - [mm] Millimeter. Resolution: 1mm. Range: [0:100000] *
* - WATER_METER_ACC_MINOR - [l] Liter. Resolution: 1l. Range: [0:99] *
* - WATER_METER_ACC_MAJOR - [hl] Hectoliter. Resolution: 1hl. Range: [0:999999] *
* - CO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - HUMIDITY_ACCURATE - [% RH] Percentage (Relative humidity). Resolution: 0.1%. Range: [0.0:100.0] *
* - STATIC_IAQ - [sIAQ] Static IAQ index. Resolution: 1sIAQ. Range: [0:10000] *
* - CO2_EQUIVALENT - [ppm] Parts per million. Resolution: 1ppm. Range: [0:1000000] *
* - BREATH_VOC - [ppm] Parts per million. Resolution: 1ppm. Range: [0:100000] *
* - CELLULAR_GATEWAY - Not applicable *
* - PERCENTAGE - [%] Percentage. Resolution: 0.01%. Range: [0.00:100.00] *
* - VOLTAGE - [mV] Millivolt. Resolution: 0.1mV. Range: [0.0:100000.0] *
* - CURRENT - [mA] Milliampere. Resolution: 0.01mA. Range: [0.00:10000.00] *
* - PULSE_CNT_ACC_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:999] *
* - PULSE_CNT_ACC_MAJOR - [kNB] Number of kilopulses. Resolution: 1 kilopulse. Range: [0:999999] *
* - ELEC_METER_ACC_MINOR - [Wh] Watt-hour. Resolution: 1Wh. Range: [0:999] *
* - ELEC_METER_ACC_MAJOR - [kWh] Kilowatt-hour. Resolution: 1kWh. Range: [0:999999] *
* - PULSE_CNT_ACC_WIDE_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [0:999999] *
* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] Number of megapulses. Resolution: 1 megapulse. Range: [0:999999] *
* - CURRENT_PRECISE - [mA] Milliampere. Resolution: 0.001mA. Range: [-4000.000:4000.000] *
* - OUTPUT_CONTROL - Not applicable *
* - RESISTANCE - [Ω] Ohm. Resolution: 1Ω. Range: [0:1000000] */
/* Encoding R: used to set relative values in rules (e.g., differential threshold and hysteresis): *
* - TEMPERATURE - [°C] Celsius degree. Resolution: 0.1°C. Range: [0.1:4273.2] *
* - HUMIDITY - [% RH] Percentage (Relative humidity). Resolution: 1%. Range: [1:100] *
* - ATMOSPHERIC_PRESSURE - [hPa] Hectopascal. Resolution: 0.1hPa. Range: [0.1:1999.0] *
* - DIFERENTIAL_PRESSURE - [Pa] Pascal. Resolution: 1Pa. Range: [1:20000] *
* - OK_ALARM - Not applicable *
* - IAQ - [IAQ] IAQ index. Resolution: 1IAQ. Range: [1:500] *
* - FLOODING - Not applicable *
* - PULSE_CNT - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:8000000]. Period number of pulses *
* - ELECTRICITY_METER - [W] Watt. Resolution: 1W. Range: [1:8000000]. Period average power consumption *
* - WATER_METER - [l/min] Liter per minute. Resolution: 1l/min. Range: [1:8000000]. Period average water *
* - SOIL_MOISTURE - [kPa] Kilopascal. Resolution: 1kPa. Range: [1:1000] *
* - CO_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - NO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - H2S_GAS - [ppm] Parts per million. Resolution: 0.01ppm. Range: [0.01:80000.00] *
* - AMBIENT_LIGHT - [lx] Lux. Resolution: 0.1lx. Range: [0.1:100000.0] *
* - PM_1_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
* - PM_2_5 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
* - PM_10_0 - [µg/m^3] Microgram per cubic meter. Resolution: 1µg/m^3. Range: [1:1000] *
* - NOISE_LEVEL - [dB] Decibel. Resolution: 0.1 dB. Range: [0.1:200.0] *
* - NH3_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - CH4_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - HIGH_PRESSURE - [kPa] Kilopascal (100kPa = 1bar). Resolution: 1kPa. Range: [1:200000] *
* - DISTANCE_MM - [mm] Millimeter. Resolution: 1mm. Range: [1:100000] *
* - WATER_METER_ACC_MINOR - [l] Liter. Resolution: 1l. Range: [1:99] *
* - WATER_METER_ACC_MAJOR - [hl] Hectoliter. Resolution: 1hl. Range: [1:999999] *
* - CO2_GAS - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - HUMIDITY_ACCURATE - [% RH] Percentage (Relative humidity). Resolution: 0.1%. Range: [0.1:100.0] *
* - STATIC_IAQ - [sIAQ] Static IAQ index. Resolution: 1sIAQ. Range: [1:10000] *
* - CO2_EQUIVALENT - [ppm] Parts per million. Resolution: 1ppm. Range: [1:1000000] *
* - BREATH_VOC - [ppm] Parts per million. Resolution: 1ppm. Range: [1:100000] *
* - CELLULAR_GATEWAY - Not applicable *
* - PERCENTAGE - [%] Percentage. Resolution: 0.01%. Range: [0.01:100.00] *
* - VOLTAGE - [mV] Millivolt. Resolution: 0.1mV. Range: [0.1:100000.0] *
* - CURRENT - [mA] Milliampere. Resolution: 0.01mA. Range: [0.01:10000.00] *
* - PULSE_CNT_ACC_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:999] *
* - PULSE_CNT_ACC_MAJOR - [kNB] Number of kilopulses. Resolution: 1 kilopulse. Range: [1:999999] *
* - ELEC_METER_ACC_MINOR - [Wh] Watt-hour. Resolution: 1Wh. Range: [1:999] *
* - ELEC_METER_ACC_MAJOR - [kWh] Kilowatt-hour. Resolution: 1kWh. Range: [1:999999] *
* - PULSE_CNT_ACC_WIDE_MINOR - [NB] Number of pulses. Resolution: 1 pulse. Range: [1:999999] *
* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] Number of megapulses. Resolution: 1 megapulse. Range: [1:999999] *
* - CURRENT_PRECISE - [mA] Milliampere. Resolution: 0.001mA. Range: [0.001:8000.000] *
* - OUTPUT_CONTROL - Not applicable *
* - RESISTANCE - [Ω] Ohm. Resolution: 1Ω. Range: [1:1000000] */
/* Condition to be checked by the device. If the condition is true, an action is triggered. */
enum Condition {
/* Invalid value */
/* Invalid value. */
CONDITION_UNSPECIFIED = 0;
/* Threshold function for given rule_id is disabled */
/* The rule is disabled. */
CONDITION_DISABLED = 1;
/* Upper threshold. Continuous sensors only. If the measurement (or average from a few measurements) is over the threshold, */
/* an action is triggered. */
/* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */
/* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */
/* parameter[2] - Triggering mode: */
/* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
/* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
/* - 3 - consecutive samples (number of consecutive samples above threshold) */
/* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
/* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
/* sample above threshold will trigger action in consecutive mode. Range: [1:10]. */
/* parameter[4] - Type of measurement (as described in MeasurementType). */
/* Upper threshold. 'Continuous' sensors only. *
* If the measurement (or average from a few measurements) is over the threshold, an action is triggered. *
* parameter[0] - Threshold value in "Encoding A" format. Must match channel type. *
* parameter[1] - Hysteresis value in "Encoding R" format. Must match channel type. Set to "0" to disable. *
* parameter[2] - Triggering mode: *
* - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
* - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
* - 3 - Consecutive samples (number of consecutive samples above threshold) *
* parameter[3] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
* samples will be calculated and compared to the threshold value in average mode or the third consecutive *
* sample above threshold will trigger action in consecutive mode. Range: [1:10]. *
* parameter[4] - Type of measurement (as described in MeasurementType). */
CONDITION_HIGH_THRESHOLD = 2;
/* Lower threshold. Continuous sensors only. If the measurement (or average from a few measurements) is below the threshold, */
/* an action is triggered. */
/* parameter[0] - Threshold value in "Encoding A" format. Must match channel type */
/* parameter[1] - Hysteresis value in "Encoding R" format. Must much channel type. Set to "0" to disable */
/* parameter[2] - Triggering mode: */
/* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
/* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
/* - 3 - consecutive samples (number of consecutive samples above threshold) */
/* parameter[3] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
/* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
/* sample below threshold will trigger action in consecutive mode. Range: [1:10]. */
/* parameter[4] - Type of measurement (as described in MeasurementType). */
/* Lower threshold. 'Continuous' sensors only. *
* If the measurement (or average from a few measurements) is below the threshold, an action is triggered. *
* parameter[0] - Threshold value in "Encoding A" format. Must match channel type. *
* parameter[1] - Hysteresis value in "Encoding R" format. Must match channel type. Set to "0" to disable. *
* parameter[2] - Triggering mode: *
* - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
* - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
* - 3 - Consecutive samples (number of consecutive samples above threshold) *
* parameter[3] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
* samples will be calculated and compared to the threshold value in average mode or the third consecutive *
* sample below threshold will trigger action in consecutive mode. Range: [1:10]. *
* parameter[4] - Type of measurement (as described in MeasurementType). */
CONDITION_LOW_THRESHOLD = 3;
/* Differential threshold. Continuous sensors only. If the absolute value of the difference between the last value sent to */
/* the server and the measurement value (or average from a few measurements) is greater or equal to the value of */
/* the threshold set, an action is triggered. */
/* parameter[0] - Threshold value in "Encoding R" format. Must match channel type */
/* parameter[1] - Triggering mode: */
/* - 1 - moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) */
/* - 2 - window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) */
/* - 3 - consecutive samples (number of consecutive samples above threshold) */
/* parameter[2] - Number of measurements for trigger determination. E.g parameter[3] equals 3, average value from three */
/* samples will be calculated and compared to the threshold value in average mode or the third consecutive */
/* sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. */
/* parameter[3] - Type of measurement (as described in MeasurementType). */
/* Differential threshold. 'Continuous' sensors only. *
* If the absolute value of the difference between the last value sent to the server and the measurement value *
* (or average from a few measurements) is greater or equal to the value of the threshold set, an action is triggered. *
* parameter[0] - Threshold value in "Encoding R" format. Must match channel type. *
* parameter[1] - Triggering mode: *
* - 1 - Moving average (a1=(n1+n2+n3)/3, a2=(n2+n3+n4)/3, etc.) *
* - 2 - Window average (a1=(n1+n2+n3)/3, a2=(n4+n5+n6)/3, etc.) *
* - 3 - Consecutive samples (number of consecutive samples above threshold) *
* parameter[2] - Number of measurements for trigger determination. E.g., parameter[3] equals 3, average value from three *
* samples will be calculated and compared to the threshold value in average mode or the third consecutive *
* sample exceeding threshold will trigger action in consecutive mode. Range: [1:10]. *
* parameter[3] - Type of measurement (as described in MeasurementType). */
CONDITION_DIFF_THRESHOLD = 4;
/* Change of binary sensor's state. Binary sensors only. Each change of the binary's sensor state will trigger an action. */
/* Change of binary sensor's state. 'Binary' sensors only. *
* Each change of the binary sensor's state will trigger an action. */
CONDITION_BINARY_CHANGE_STATE = 5;
/* Logic operator. Used for combining multiple rules into more complex conditions. If the logic condition specified by */
/* parameters (logic operator and selected rules) is met, an action is triggered. */
/* parameter[0] - Logic operator (as described in LogicOperation). */
/* parameter[1] - Rule selector (bit mask). Specifies which rules should be taken into account while determining */
/* rules outcome. */
/* parameter[2] - Rule negation (bit mask). Specifies which of chosen in parameter[1] rules should be negated */
/* before determining rules outcome. */
/* parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. */
/* Range: [0:864000]. */
/* parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. */
/* Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */
/* Logic operator. Used for combining multiple rules into more complex conditions. *
* If the logic condition specified by parameters (logic operator and selected rules) is met, an action is triggered. *
* parameter[0] - Logic operator (as described in LogicOperator). *
* parameter[1] - Rule selector (bitmask). Specifies which rules should be taken into account while determining *
* rules outcome. *
* parameter[2] - Rule negation (bitmask). Specifies which of chosen in parameter[1] rules should be negated *
* before determining rules outcome. *
* parameter[3] - Rule action delay [s]. Specifies time delay between the rule activation and rule action being triggered. *
* Range: [0:864000]. *
* parameter[4] - Rule return delay [s]. Specifies time delay between the rule deactivation and rule action being triggered. *
* Range: [0:864001]. Max parameter value disables action triggering on rule deactivation. */
CONDITION_LOGIC_OPERATOR = 6;
/* On measurement. Continous sensors only. The basic function is to trigger communication after measurement if at least 60s */
/* have passed since the last one. Transmission may occur every x measurement. Optionally dependency on the other rule can */
/* be configured, then, when all conditions are met, transmission is triggered. */
/* parameter[0] - Send every n measurement. This parameter specifies every which measurement transmission will be triggered */
/* if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will occur after */
/* every measurement. */
/* parameter[1] - Optional. Rule selector (bit mask). Specifies which rule should be taken into account while determining */
/* the measurement rule outcome. */
/* parameter[2] - Optional. Rule negation (bit mask). Specifies which of chosen in parameter[1] rule should be negated */
/* before determining the measurement rule outcome. */
/* On measurement. 'Continous' sensors only.
* The basic function is to trigger communication after measurement if at least 60s have passed since the last one. *
* Transmission may occur every n-th measurement. Optionally dependency on the other rule can be configured, then, when all *
* conditions are met, transmission is triggered. *
* parameter[0] - Send every n-th measurement. This parameter specifies every which measurement transmission will be *
* triggered if all other conditions are met. Range: [1:500]. If parameter[0] equals 1, transmission will *
* occur after every measurement. *
* parameter[1] - Optional. Rule selector (bitmask). Specifies which rule should be taken into account while determining *
* the measurement rule outcome. *
* parameter[2] - Optional. Rule negation (bitmask). Specifies which of the rules selected in parameter[1] should be negated *
* before determining the measurement rule outcome. */
CONDITION_ON_MEASUREMENT = 7;
/* On sensor error. 'Continous' sensors only. *
* If the sensor returns an error code, an action will be triggered. *
* The rule becomes inactive when a correct measurement appears. */
CONDITION_ON_SENSOR_ERROR = 8;
}
/* Logic operators to be used for determining the outcome of rules with logic operator condition. */
enum LogicOperator {
/* Invalid use */
/* Invalid value. */
LOGIC_OPERATOR_UNSPECIFIED = 0;
/* Logic AND */
/* Logic AND. */
LOGIC_OPERATOR_AND = 1;
/* Logic OR */
/* Logic OR. */
LOGIC_OPERATOR_OR = 2;
}
/* Action to be triggered. Currently the only possible action is to trigger the transmission. */
/* Other actions will be available in next SW releases. */
/* Action to be triggered. */
enum Action {
/* Invalid value */
/* Invalid value. */
ACTION_UNSPECIFIED = 0;
/* To trigger the transmission */
/* To trigger the transmission. */
ACTION_TRIGGER_TRANSMISSION = 1;
/* To take no action. Possible for logic operator components */
/* To take no action. Possible for logic operator components. */
ACTION_NO_ACTION = 2;
/* To trigger the transmission with ACK */
/* To trigger the transmission with ACK. */
ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3;
/* To change BLE advertising period mode to fast (with lower user-configured advertising interval). */
/* Once the rule is deactived avertising period mode returns to previously configured value. */
/* To change BLE advertising period mode to fast (with lower user-configured advertising interval). *
* Once the rule is deactived avertising period mode returns to previously configured value. */
ACTION_FAST_ADVERTISING_MODE = 4;
}
/* Type of a rule calendars. */
enum CalendarType {
/* Invalid value */
/* Invalid value. */
CALENDAR_TYPE_UNSPECIFIED = 0;
/* Type for inactive calendars */
/* Calendar is inactive. */
CALENDAR_TYPE_DISABLED = 1;
/* Week type. Enables selcted rules on specified days of the week in specified time periods. */
/* parameter[0] - Week day mask. Bitmask of days when selected rules are enabled */
/* - Bit 0 - Sunday */
/* - Bit 1 - Monday */
/* ... */
/* - Bit 6 - Saturday */
/* parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). */
/* parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). */
/* Note: if 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' */
/* and from 'From time' to 23:59. */
/* parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets */
/* - example - if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. */
/* Week calendar. Enables selcted rules on specified days of the week in specified time periods. *
* parameter[0] - Week day mask. Bitmask of days when selected rules are enabled: *
* - Bit 0 - Sunday *
* - Bit 1 - Monday *
* ... *
* - Bit 6 - Saturday *
* parameter[1] - 'From time' - point in time from which selected rules will be enabled (in minutes from midnight). *
* parameter[2] - 'To time' - point in time from which selected rules will be disabled (in minutes from midnight). *
* parameter[3] - Timezone - desired timezone for date comparison. Encoded as number (N) of 15 minutes offsets, *
* for example: if N = 4, then offset = 4 * 15min = 1h. I.e. timezone is UTC+1. *
* Note: If 'From time' is bigger than 'To time' there are two periods when rules are enabled - from 00:00 to 'To time' *
* and from 'From time' to 23:59. */
CALENDAR_TYPE_WEEK = 2;
}
/* Rules calendars. Used for enabling/disabling rules based on date/time. */
/* It is possible to configure up to 6 calendars. Each of them can affect any number of rules. */
message ProtoCalendar {
/* Bit mask of selected rules. Mask on bits [0:11] */
/* - Bit 0 - Rule ID 0 */
/* - Bit 1 - Rule ID 1 */
/* ... */
/* - Bit 11 - Rule ID 11 */
/* Selected rules. *
* Bitmask - up to 16 rules supported (previously 12 rules [06.00 - 07.01.xx]): *
* - Bit 0 - Rule 1 *
* - Bit 1 - Rule 2 *
* ... *
* - Bit 15 - Rule 16 *
* Status: In use [06.08.00 - LATEST] */
uint32 rule_mask = 1;
/* Calendars's parameters. Described in Type. */
/* Calendar parameters (as described in CalendarType). *
* Status: In use [06.08.00 - LATEST] */
repeated sint32 parameters = 2;
/* Calendar's type. Described in Type. */
/* Calendar type (as described in CalendarType). *
* Status: In use [06.08.00 - LATEST] */
CalendarType type = 3;
}
/* Rules used to define edge logic on the device. Rules are defined by conditions and actions: */
/* If Condition is true, trigger Action. It is possible to configure up to 12 rules and assign them to different channels. */
/* One rule can be assigned to any number of channels. For instance rule "If temperature is over 10 C, trigger the transmission"*/
/* can be assigned to channels 1 and 2. No matter to how many channels a rule is assigned, it's still counted as one rule. */
/* Rules are used to define edge logic on the device. Rules are defined by conditions and actions. *
* If 'condition' is true, trigger 'action'. One rule can be assigned to any number of channels. *
* For instance rule "If temperature is over 10 C, trigger the transmission" can be assigned to channels 1 and 2. *
* No matter to how many channels a rule is assigned, it's still counted as one rule. */
message ProtoRule {
/* Channels to which the rule is assigned. One rule can be assigned to multiple channels as long as those are of the same type*/
/* Bit mask on bits [0:5]. E.g. To assign the rule for channel 1: "000001", to assign rule to channels 2 and 4: "001010" */
/* Channels to which the rule is assigned. *
* One rule can be assigned to multiple channels as long as those are of the same type. *
* Bitmask: *
* - Bit 0 - Channel 1 *
* - Bit 1 - Channel 2 *
* ... *
* - Bit 5 - Channel 6 *
* Status: In use [06.00 - LATEST] */
uint32 channel_mask = 1;
/* Rule's condition (as described in Condition). */
/* Rule condition (as described in Condition). *
* Status: In use [06.00 - LATEST] */
Condition condition = 2;
/* Condition's parameters (as described in Condition). For binary sensors there are no parameters */
/* Condition parameters (as described in Condition). *
* For 'Binary' sensors there are no parameters. *
* Status: In use [06.00 - LATEST] */
repeated sint32 parameters = 3;
/* Action to be triggered. */
/* Action to be triggered. *
* Status: In use [06.00 - LATEST] */
Action action = 4;
}

664
common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResourceTest.java

@ -16,23 +16,32 @@
package org.thingsboard.server.transport.coap.efento;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.thingsboard.server.gen.transport.coap.ConfigProtos;
import org.thingsboard.server.gen.transport.coap.ConfigTypesProtos;
import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoMeasurements;
import org.thingsboard.server.gen.transport.coap.ProtoRuleProtos;
import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@ -85,7 +94,7 @@ class CoapEfentoTransportResourceTest {
void checkContinuousSensorWithSomeMeasurements() {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(5)
@ -93,7 +102,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
.setTimestamp(Math.toIntExact(tsInSec))
@ -114,7 +123,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 180 * 5) * 1000);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.4);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(30);
checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 5, false);
checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 5);
}
@ParameterizedTest
@ -122,7 +131,7 @@ class CoapEfentoTransportResourceTest {
void checkContinuousSensor(MeasurementType measurementType, List<Integer> sampleOffsets, String property, double expectedValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@ -130,7 +139,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(measurementType)
.setTimestamp(Math.toIntExact(tsInSec))
@ -142,7 +151,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(property).getAsDouble()).isEqualTo(expectedValue);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream<Arguments> checkContinuousSensor() {
@ -176,7 +185,7 @@ class CoapEfentoTransportResourceTest {
String totalPropertyName, double expectedTotalValue) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@ -184,7 +193,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.addAllChannels(Arrays.asList(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(majorType)
.setTimestamp(Math.toIntExact(tsInSec))
@ -200,7 +209,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(totalPropertyName + "_2").getAsDouble()).isEqualTo(expectedTotalValue);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, false);
checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream<Arguments> checkPulseCounterSensors() {
@ -220,7 +229,7 @@ class CoapEfentoTransportResourceTest {
void checkBinarySensor() {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(0)
@ -228,7 +237,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(MEASUREMENT_TYPE_OK_ALARM)
.setTimestamp(Math.toIntExact(tsInSec))
@ -239,7 +248,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements).hasSize(1);
assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000);
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM");
checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 14, true);
checkDefaultMeasurements(measurements, efentoMeasurements, 180 * 14);
}
@ParameterizedTest
@ -247,7 +256,7 @@ class CoapEfentoTransportResourceTest {
void checkBinarySensorWhenValueIsVarying(MeasurementType measurementType, String property, String expectedValueWhenOffsetNotOk, String expectedValueWhenOffsetOk) {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(1)
@ -255,7 +264,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(measurementType)
.setTimestamp(Math.toIntExact(tsInSec))
@ -268,7 +277,7 @@ class CoapEfentoTransportResourceTest {
assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get(property).getAsString()).isEqualTo(expectedValueWhenOffsetNotOk);
assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 9) * 1000);
assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get(property).getAsString()).isEqualTo(expectedValueWhenOffsetOk);
checkDefaultMeasurements(measurements, efentoMeasurements, 180, true);
checkDefaultMeasurements(measurements, efentoMeasurements, 180);
}
private static Stream<Arguments> checkBinarySensorWhenValueIsVarying() {
@ -282,7 +291,7 @@ class CoapEfentoTransportResourceTest {
@Test
void checkExceptionWhenChannelsListIsEmpty() {
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(1)
@ -290,7 +299,7 @@ class CoapEfentoTransportResourceTest {
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.setConfigurationHash(0)
.build();
UUID sessionId = UUID.randomUUID();
@ -299,29 +308,587 @@ class CoapEfentoTransportResourceTest {
.hasMessage("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!");
}
// -------------------------------------------------------------------------
// ProtoDeviceInfo parsing tests
// -------------------------------------------------------------------------
@Test
void checkExceptionWhenValuesMapIsEmpty() {
long tsInSec = Instant.now().getEpochSecond();
ProtoMeasurements measurements = ProtoMeasurements.newBuilder()
.setSerialNum(integerToByteString(1234))
.setCloudToken("test_token")
.setMeasurementPeriodBase(180)
.setMeasurementPeriodFactor(1)
.setBatteryStatus(true)
.setSignal(0)
.setNextTransmissionAt(1000)
.setTransferReason(0)
.setHash(0)
.addChannels(MeasurementsProtos.ProtoChannel.newBuilder()
.setType(MEASUREMENT_TYPE_TEMPERATURE)
.setTimestamp(Math.toIntExact(tsInSec))
void getEfentoDeviceInfo_parsesSwVersion() {
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setSwVersion(1546) // ver 06.10 => 0x060A => 1546
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
assertThat(result.getValues().getAsJsonObject().get("sw_version").getAsInt()).isEqualTo(1546);
}
@Test
void getEfentoDeviceInfo_parsesAllMemoryStatistics() {
// proto uint32 maps to Java int; the "undefined" sentinel is 0xFFFFFFFF = -1 in signed int
int undefinedTs = -1;
int knownTs = 1_700_000_000; // arbitrary known Unix timestamp fitting in uint32
// clearMemoryStatistics() is needed because minimalDeviceInfo() pre-populates 22 zeros;
// addAllMemoryStatistics() appends in proto, so without clear the test values land at indices 22-43
DeviceInfoProtos.ProtoDeviceInfo.Builder builder = minimalDeviceInfo().clearMemoryStatistics();
builder.addAllMemoryStatistics(List.of(
0, // [0] nv_storage_status: 0 = no errors
knownTs, // [1] timestamp_of_the_end_of_collecting_statistics
1048576, // [2] capacity_of_memory_in_bytes
512000, // [3] used_space_in_bytes
1024, // [4] size_of_invalid_packets_in_bytes
256, // [5] size_of_corrupted_packets_in_bytes
100, // [6] number_of_valid_packets
10, // [7] number_of_invalid_packets
2, // [8] number_of_corrupted_packets
500, // [9] number_of_all_samples_for_channel_1
400, // [10] number_of_all_samples_for_channel_2
300, // [11] number_of_all_samples_for_channel_3
200, // [12] number_of_all_samples_for_channel_4
100, // [13] number_of_all_samples_for_channel_5
50, // [14] number_of_all_samples_for_channel_6
undefinedTs, // [15] timestamp_of_the_first_binary_measurement (undefined)
undefinedTs, // [16] timestamp_of_the_last_binary_measurement (undefined)
undefinedTs, // [17] timestamp_of_the_first_binary_measurement_sent (undefined)
knownTs, // [18] timestamp_of_the_first_continuous_measurement
knownTs, // [19] timestamp_of_the_last_continuous_measurement
knownTs, // [20] timestamp_of_the_last_continuous_measurement_sent
42 // [21] nvm_write_counter
));
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(builder.build());
var json = result.getValues().getAsJsonObject();
assertThat(json.get("nv_storage_status").getAsLong()).isEqualTo(0);
assertThat(json.get("timestamp_of_the_end_of_collecting_statistics").getAsString()).isEqualTo(formatDate(knownTs));
assertThat(json.get("capacity_of_memory_in_bytes").getAsLong()).isEqualTo(1048576L);
assertThat(json.get("used_space_in_bytes").getAsLong()).isEqualTo(512000L);
assertThat(json.get("size_of_invalid_packets_in_bytes").getAsLong()).isEqualTo(1024L);
assertThat(json.get("size_of_corrupted_packets_in_bytes").getAsLong()).isEqualTo(256L);
assertThat(json.get("number_of_valid_packets").getAsLong()).isEqualTo(100L);
assertThat(json.get("number_of_invalid_packets").getAsLong()).isEqualTo(10L);
assertThat(json.get("number_of_corrupted_packets").getAsLong()).isEqualTo(2L);
assertThat(json.get("number_of_all_samples_for_channel_1").getAsLong()).isEqualTo(500L);
assertThat(json.get("number_of_all_samples_for_channel_2").getAsLong()).isEqualTo(400L);
assertThat(json.get("number_of_all_samples_for_channel_3").getAsLong()).isEqualTo(300L);
assertThat(json.get("number_of_all_samples_for_channel_4").getAsLong()).isEqualTo(200L);
assertThat(json.get("number_of_all_samples_for_channel_5").getAsLong()).isEqualTo(100L);
assertThat(json.get("number_of_all_samples_for_channel_6").getAsLong()).isEqualTo(50L);
assertThat(json.get("timestamp_of_the_first_binary_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_last_binary_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_first_binary_measurement_sent").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_first_continuous_measurement").getAsString()).isEqualTo(formatDate(knownTs));
assertThat(json.get("timestamp_of_the_last_continuous_measurement").getAsString()).isEqualTo(formatDate(knownTs));
assertThat(json.get("timestamp_of_the_last_continuous_measurement_sent").getAsString()).isEqualTo(formatDate(knownTs));
assertThat(json.get("nvm_write_counter").getAsLong()).isEqualTo(42L);
}
@Test
void getEfentoDeviceInfo_parsesModemInfo() {
// 34 modem parameters (indices 0-33) as defined in proto_device_info.proto
List<Integer> params = List.of(
0, // [0] sc_EARNFCN_offset
1000, // [1] sc_EARFCN
42, // [2] sc_PCI
123456, // [3] sc_Cell_id
-90, // [4] sc_RSRP
-10, // [5] sc_RSRQ
-80, // [6] sc_RSSI
15, // [7] sc_SINR
3, // [8] sc_Band
1234, // [9] sc_TAC
1, // [10] sc_ECL
-30, // [11] sc_TX_PWR
2, // [12] op_mode
999, // [13] nc_EARFCN
1, // [14] nc_EARNFCN_offset
100, // [15] nc_PCI
-95, // [16] nc_RSRP
5, // [17] RLC_UL_BLER
3, // [18] RLC_DL_BLER
4, // [19] MAC_UL_BLER
2, // [20] MAC_DL_BLER
50000,// [21] MAC_UL_TOTAL_BYTES
60000,// [22] MAC_DL_TOTAL_BYTES
200, // [23] MAC_UL_total_HARQ_Tx
150, // [24] MAC_DL_total_HARQ_Tx
10, // [25] MAC_UL_HARQ_re_Tx
8, // [26] MAC_DL_HARQ_re_Tx
1000, // [27] RLC_UL_tput
1200, // [28] RLC_DL_tput
900, // [29] MAC_UL_tput
1100, // [30] MAC_DL_tput
5000, // [31] sleep_duration
300, // [32] rx_time
100 // [33] tx_time
);
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setModem(DeviceInfoProtos.ProtoModem.newBuilder()
.setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC66)
.addAllParameters(params)
.build())
.build();
UUID sessionId = UUID.randomUUID();
assertThatThrownBy(() -> coapEfentoTransportResource.getEfentoMeasurements(measurements, sessionId))
.isInstanceOf(IllegalStateException.class)
.hasMessage("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_BC66");
assertThat(json.get("sc_EARNFCN_offset").getAsInt()).isEqualTo(0);
assertThat(json.get("sc_EARFCN").getAsInt()).isEqualTo(1000);
assertThat(json.get("sc_PCI").getAsInt()).isEqualTo(42);
assertThat(json.get("sc_Cell_id").getAsInt()).isEqualTo(123456);
assertThat(json.get("sc_RSRP").getAsInt()).isEqualTo(-90);
assertThat(json.get("sc_RSRQ").getAsInt()).isEqualTo(-10);
assertThat(json.get("sc_RSSI").getAsInt()).isEqualTo(-80);
assertThat(json.get("sc_SINR").getAsInt()).isEqualTo(15);
assertThat(json.get("sc_Band").getAsInt()).isEqualTo(3);
assertThat(json.get("sc_TAC").getAsInt()).isEqualTo(1234);
assertThat(json.get("sc_ECL").getAsInt()).isEqualTo(1);
assertThat(json.get("sc_TX_PWR").getAsInt()).isEqualTo(-30);
assertThat(json.get("op_mode").getAsInt()).isEqualTo(2);
assertThat(json.get("nc_EARFCN").getAsInt()).isEqualTo(999);
assertThat(json.get("nc_EARNFCN_offset").getAsInt()).isEqualTo(1);
assertThat(json.get("nc_PCI").getAsInt()).isEqualTo(100);
assertThat(json.get("nc_RSRP").getAsInt()).isEqualTo(-95);
assertThat(json.get("RLC_UL_BLER").getAsInt()).isEqualTo(5);
assertThat(json.get("RLC_DL_BLER").getAsInt()).isEqualTo(3);
assertThat(json.get("MAC_UL_BLER").getAsInt()).isEqualTo(4);
assertThat(json.get("MAC_DL_BLER").getAsInt()).isEqualTo(2);
assertThat(json.get("MAC_UL_TOTAL_BYTES").getAsInt()).isEqualTo(50000);
assertThat(json.get("MAC_DL_TOTAL_BYTES").getAsInt()).isEqualTo(60000);
assertThat(json.get("MAC_UL_total_HARQ_Tx").getAsInt()).isEqualTo(200);
assertThat(json.get("MAC_DL_total_HARQ_Tx").getAsInt()).isEqualTo(150);
assertThat(json.get("MAC_UL_HARQ_re_Tx").getAsInt()).isEqualTo(10);
assertThat(json.get("MAC_DL_HARQ_re_Tx").getAsInt()).isEqualTo(8);
assertThat(json.get("RLC_UL_tput").getAsInt()).isEqualTo(1000);
assertThat(json.get("RLC_DL_tput").getAsInt()).isEqualTo(1200);
assertThat(json.get("MAC_UL_tput").getAsInt()).isEqualTo(900);
assertThat(json.get("MAC_DL_tput").getAsInt()).isEqualTo(1100);
assertThat(json.get("sleep_duration").getAsInt()).isEqualTo(5000);
assertThat(json.get("rx_time").getAsInt()).isEqualTo(300);
assertThat(json.get("tx_time").getAsInt()).isEqualTo(100);
}
@Test
void getEfentoDeviceInfo_parsesModemInfoBC660() {
// 22 modem parameters for MODEM_TYPE_BC660 as defined in proto_device_info.proto
List<Integer> params = List.of(
1000, // [0] sc_EARFCN
5, // [1] sc_EARNFCN_offset
42, // [2] sc_PCI
123456, // [3] sc_Cell_id
-90, // [4] sc_RSRP
-10, // [5] sc_RSRQ
-80, // [6] sc_RSSI
15, // [7] sc_SINR
3, // [8] sc_Band
1234, // [9] sc_TAC
1, // [10] sc_ECL
-30, // [11] sc_TX_PWR
2, // [12] op_mode
999, // [13] nc_EARFCN
100, // [14] nc_PCI
-95, // [15] nc_RSRP
-12, // [16] nc_RSRQ
5000, // [17] sleep_duration
300, // [18] rx_time
100, // [19] tx_time
1, // [20] PLMN_state
26201 // [21] select_PLMN
);
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setModem(DeviceInfoProtos.ProtoModem.newBuilder()
.setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC660)
.addAllParameters(params)
.setSimCardIdentification("89001012012341234120")
.setFirmwareVersion(DeviceInfoProtos.ModemFirmwareVersion.MODEM_FIRMWARE_VERSION_BC660_V2)
.setModemIdentification("123456789012345")
.addAllModemStatistics(List.of(10, 3600, 7200, 600))
.build())
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_BC660");
assertThat(json.get("sim_card_identification").getAsString()).isEqualTo("89001012012341234120");
assertThat(json.get("firmware_version").getAsString()).isEqualTo("MODEM_FIRMWARE_VERSION_BC660_V2");
assertThat(json.get("modem_identification").getAsString()).isEqualTo("123456789012345");
assertThat(json.get("modem_transmissions_count").getAsInt()).isEqualTo(10);
assertThat(json.get("modem_time_since_last_devinfo").getAsInt()).isEqualTo(3600);
assertThat(json.get("modem_total_psm_time").getAsInt()).isEqualTo(7200);
assertThat(json.get("modem_total_active_time").getAsInt()).isEqualTo(600);
assertThat(json.get("sc_EARFCN").getAsInt()).isEqualTo(1000);
assertThat(json.get("sc_EARNFCN_offset").getAsInt()).isEqualTo(5);
assertThat(json.get("sc_PCI").getAsInt()).isEqualTo(42);
assertThat(json.get("sc_Cell_id").getAsInt()).isEqualTo(123456);
assertThat(json.get("sc_RSRP").getAsInt()).isEqualTo(-90);
assertThat(json.get("sc_RSRQ").getAsInt()).isEqualTo(-10);
assertThat(json.get("sc_RSSI").getAsInt()).isEqualTo(-80);
assertThat(json.get("sc_SINR").getAsInt()).isEqualTo(15);
assertThat(json.get("sc_Band").getAsInt()).isEqualTo(3);
assertThat(json.get("sc_TAC").getAsInt()).isEqualTo(1234);
assertThat(json.get("sc_ECL").getAsInt()).isEqualTo(1);
assertThat(json.get("sc_TX_PWR").getAsInt()).isEqualTo(-30);
assertThat(json.get("op_mode").getAsInt()).isEqualTo(2);
assertThat(json.get("nc_EARFCN").getAsInt()).isEqualTo(999);
assertThat(json.get("nc_PCI").getAsInt()).isEqualTo(100);
assertThat(json.get("nc_RSRP").getAsInt()).isEqualTo(-95);
assertThat(json.get("nc_RSRQ").getAsInt()).isEqualTo(-12);
assertThat(json.get("sleep_duration").getAsInt()).isEqualTo(5000);
assertThat(json.get("rx_time").getAsInt()).isEqualTo(300);
assertThat(json.get("tx_time").getAsInt()).isEqualTo(100);
assertThat(json.get("PLMN_state").getAsInt()).isEqualTo(1);
assertThat(json.get("select_PLMN").getAsInt()).isEqualTo(26201);
// BC66-specific fields must not be present
assertThat(json.has("nc_EARNFCN_offset")).isFalse();
assertThat(json.has("RLC_UL_BLER")).isFalse();
}
@Test
void getEfentoDeviceInfo_parsesModemInfoSharedModem() {
// 4 modem parameters for MODEM_TYPE_SHARED_MODEM as defined in proto_device_info.proto
List<Integer> params = List.of(
-90, // [0] RSRP
-10, // [1] RSRQ
-80, // [2] RSSI
15 // [3] SINR
);
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setModem(DeviceInfoProtos.ProtoModem.newBuilder()
.setType(DeviceInfoProtos.ModemType.MODEM_TYPE_SHARED_MODEM)
.addAllParameters(params)
.setModemIdentification("SN-ABCDEF")
.build())
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("modem_types").getAsString()).isEqualTo("MODEM_TYPE_SHARED_MODEM");
assertThat(json.get("modem_identification").getAsString()).isEqualTo("SN-ABCDEF");
assertThat(json.get("RSRP").getAsInt()).isEqualTo(-90);
assertThat(json.get("RSRQ").getAsInt()).isEqualTo(-10);
assertThat(json.get("RSSI").getAsInt()).isEqualTo(-80);
assertThat(json.get("SINR").getAsInt()).isEqualTo(15);
// BC66/BC660-specific fields must not be present
assertThat(json.has("sc_EARFCN")).isFalse();
assertThat(json.has("sc_EARNFCN_offset")).isFalse();
}
@Test
void getEfentoDeviceInfo_parsesNewModemFields() {
// New ProtoModem fields: sim_card_identification, firmware_version, modem_identification, modem_statistics
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setModem(DeviceInfoProtos.ProtoModem.newBuilder()
.setType(DeviceInfoProtos.ModemType.MODEM_TYPE_BC66)
.addAllParameters(java.util.Collections.nCopies(34, 0))
.setSimCardIdentification("89012345678901234567")
.setFirmwareVersion(DeviceInfoProtos.ModemFirmwareVersion.MODEM_FIRMWARE_VERSION_READING_ERROR)
.setModemIdentification("352519100417272")
.addAllModemStatistics(List.of(5, 1800, 3600, 120))
.build())
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("sim_card_identification").getAsString()).isEqualTo("89012345678901234567");
assertThat(json.get("firmware_version").getAsString()).isEqualTo("MODEM_FIRMWARE_VERSION_READING_ERROR");
assertThat(json.get("modem_identification").getAsString()).isEqualTo("352519100417272");
assertThat(json.get("modem_transmissions_count").getAsInt()).isEqualTo(5);
assertThat(json.get("modem_time_since_last_devinfo").getAsInt()).isEqualTo(1800);
assertThat(json.get("modem_total_psm_time").getAsInt()).isEqualTo(3600);
assertThat(json.get("modem_total_active_time").getAsInt()).isEqualTo(120);
}
@Test
void getEfentoDeviceInfo_parsesRuntimeInfo() {
long batteryResetTs = 1_700_000_000L;
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo()
.setRuntimeInfo(DeviceInfoProtos.ProtoRuntime.newBuilder()
.setUpTime(3600)
.addAllMessageCounters(List.of(10, 5, 9))
.setMcuTemperature(25)
.setBatteryVoltage(3200)
.setMinBatteryMcuTemperature(20)
.setBatteryResetTimestamp((int) batteryResetTs)
.setMaxMcuTemperature(40)
.setMinMcuTemperature(10)
.addAllRuntimeErrors(List.of(0, 1))
.build())
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("up_time").getAsLong()).isEqualTo(3600);
assertThat(json.get("mcu_temp").getAsInt()).isEqualTo(25);
assertThat(json.get("min_battery_voltage").getAsLong()).isEqualTo(3200);
assertThat(json.get("min_battery_mcu_temp").getAsInt()).isEqualTo(20);
assertThat(json.get("battery_reset_timestamp").getAsString()).isEqualTo(formatDate(batteryResetTs));
assertThat(json.get("max_mcu_temp").getAsInt()).isEqualTo(40);
assertThat(json.get("min_mcu_temp").getAsInt()).isEqualTo(10);
assertThat(json.get("counter_of_confirmable_messages_attempts").getAsInt()).isEqualTo(10);
assertThat(json.get("counter_of_non_confirmable_messages_attempts").getAsInt()).isEqualTo(5);
assertThat(json.get("counter_of_succeeded_messages").getAsInt()).isEqualTo(9);
assertThat(json.get("runtime_errors").getAsInt()).isEqualTo(2); // count of errors, not values
}
@Test
void getEfentoDeviceInfo_undefinedTimestampRenderedAsUndefinedString() {
// -1 as signed int == 0xFFFFFFFF == uint32 max (4294967295), the "Undefined" sentinel
DeviceInfoProtos.ProtoDeviceInfo deviceInfo = minimalDeviceInfo().clearMemoryStatistics()
.addAllMemoryStatistics(List.of(
0, -1, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
-1, -1, -1,
-1, -1, -1,
0))
.build();
CoapEfentoTransportResource.EfentoTelemetry result = coapEfentoTransportResource.getEfentoDeviceInfo(deviceInfo);
var json = result.getValues().getAsJsonObject();
assertThat(json.get("timestamp_of_the_end_of_collecting_statistics").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_first_binary_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_last_binary_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_first_binary_measurement_sent").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_first_continuous_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_last_continuous_measurement").getAsString()).isEqualTo("Undefined");
assertThat(json.get("timestamp_of_the_last_continuous_measurement_sent").getAsString()).isEqualTo("Undefined");
}
@Test
void getEfentoDeviceInfo_tsIsCurrentTimeMillis() {
long before = System.currentTimeMillis();
CoapEfentoTransportResource.EfentoTelemetry result =
coapEfentoTransportResource.getEfentoDeviceInfo(minimalDeviceInfo().build());
long after = System.currentTimeMillis();
assertThat(result.getTs()).isBetween(before, after);
}
// -------------------------------------------------------------------------
// ProtoConfig / getEfentoConfiguration parsing tests
// -------------------------------------------------------------------------
@Test
void getEfentoConfiguration_parsesServerCommunicationFields() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setDataServerIp("18.184.24.239")
.setDataServerPort(5683)
.setUpdateServerIp("efento.update.io")
.setUpdateServerPortCoap(5684)
.setUpdateServerPortUdp(5685)
.setTransmissionInterval(300)
.setAckInterval(600)
.setSupervisionPeriod(3600)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("dataServerIp").getAsString()).isEqualTo("18.184.24.239");
assertThat(json.get("dataServerPort").getAsLong()).isEqualTo(5683);
assertThat(json.get("updateServerIp").getAsString()).isEqualTo("efento.update.io");
assertThat(json.get("updateServerPortCoap").getAsLong()).isEqualTo(5684);
assertThat(json.get("updateServerPortUdp").getAsLong()).isEqualTo(5685);
assertThat(json.get("transmissionInterval").getAsLong()).isEqualTo(300);
assertThat(json.get("ackInterval").getAsLong()).isEqualTo(600);
assertThat(json.get("supervisionPeriod").getAsLong()).isEqualTo(3600);
}
@Test
void getEfentoConfiguration_parsesMeasurementPeriod() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setMeasurementPeriodBase(60)
.setMeasurementPeriodFactor(5)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("measurementPeriodBase").getAsLong()).isEqualTo(60);
assertThat(json.get("measurementPeriodFactor").getAsLong()).isEqualTo(5);
}
@Test
void getEfentoConfiguration_parsesBooleanRequestFields() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setDeviceInfoRequest(true)
.setUpdateSoftwareRequest(true)
.setConfigurationRequest(true)
.setAcceptWithoutTestingRequest(true)
.setResetMemoryRequest(true)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("deviceInfoRequest").getAsBoolean()).isTrue();
assertThat(json.get("updateSoftwareRequest").getAsBoolean()).isTrue();
assertThat(json.get("configurationRequest").getAsBoolean()).isTrue();
assertThat(json.get("acceptWithoutTestingRequest").getAsBoolean()).isTrue();
assertThat(json.get("resetMemoryRequest").getAsBoolean()).isTrue();
}
@Test
void getEfentoConfiguration_parsesModemAndNetworkFields() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setApn("internet.example.com")
.setApnUsername("user")
.setApnPassword("pass")
.setPlmnSelection(26001)
.setModemBandsMask(2084) // bands 3, 8, 20
.setNetworkTroubleshooting(2)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("apn").getAsString()).isEqualTo("internet.example.com");
assertThat(json.get("apnUsername").getAsString()).isEqualTo("user");
assertThat(json.get("apnPassword").getAsString()).isEqualTo("pass");
assertThat(json.get("plmnSelection").getAsLong()).isEqualTo(26001);
assertThat(json.get("modemBandsMask").getAsLong()).isEqualTo(2084);
assertThat(json.get("networkTroubleshooting").getAsLong()).isEqualTo(2);
}
@Test
void getEfentoConfiguration_parsesCloudTokenFields() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setCloudToken("my-device-token")
.setCloudTokenConfig(1)
.setCloudTokenCoapOption(65000)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("cloudToken").getAsString()).isEqualTo("my-device-token");
assertThat(json.get("cloudTokenConfig").getAsLong()).isEqualTo(1);
assertThat(json.get("cloudTokenCoapOption").getAsLong()).isEqualTo(65000);
}
@Test
void getEfentoConfiguration_parsesEndpointFields() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setDataEndpoint("/m")
.setConfigurationEndpoint("/c")
.setDeviceInfoEndpoint("/i")
.setTimeEndpoint("/t")
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
assertThat(json.get("dataEndpoint").getAsString()).isEqualTo("/m");
assertThat(json.get("configurationEndpoint").getAsString()).isEqualTo("/c");
assertThat(json.get("deviceInfoEndpoint").getAsString()).isEqualTo("/i");
assertThat(json.get("timeEndpoint").getAsString()).isEqualTo("/t");
}
@Test
void getEfentoConfiguration_parsesBleAdvertisingPeriod() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.setBleAdvertisingPeriod(ConfigTypesProtos.ProtoBleAdvertisingPeriod.newBuilder()
.setMode(ConfigTypesProtos.BleAdvertisingPeriodMode.BLE_ADVERTISING_PERIOD_MODE_NORMAL)
.setNormal(1600)
.setFast(320)
.build())
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
var ble = json.get("bleAdvertisingPeriod").getAsJsonObject();
assertThat(ble.get("mode").getAsString()).isEqualTo("BLE_ADVERTISING_PERIOD_MODE_NORMAL");
assertThat(ble.get("normal").getAsLong()).isEqualTo(1600);
assertThat(ble.get("fast").getAsLong()).isEqualTo(320);
}
@Test
void getEfentoConfiguration_parsesRepeatedChannelTypes() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.addChannelTypes(MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
.addChannelTypes(MeasurementType.MEASUREMENT_TYPE_HUMIDITY)
.addChannelTypes(MeasurementType.MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE)
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
var channelTypes = json.get("channelTypes").getAsJsonArray();
assertThat(channelTypes).hasSize(3);
assertThat(channelTypes.get(0).getAsString()).isEqualTo("MEASUREMENT_TYPE_TEMPERATURE");
assertThat(channelTypes.get(1).getAsString()).isEqualTo("MEASUREMENT_TYPE_HUMIDITY");
assertThat(channelTypes.get(2).getAsString()).isEqualTo("MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE");
}
@Test
void getEfentoConfiguration_parsesEdgeLogicRules() throws InvalidProtocolBufferException {
// ProtoRule uses channel_mask (bit mask), condition, parameters, action
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder()
.addRules(ProtoRuleProtos.ProtoRule.newBuilder()
.setChannelMask(1) // channel 1 → bit mask 0b000001
.setCondition(ProtoRuleProtos.Condition.CONDITION_HIGH_THRESHOLD)
.addParameters(500) // threshold value
.setAction(ProtoRuleProtos.Action.ACTION_TRIGGER_TRANSMISSION)
.build())
.build()
.toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
var rules = json.get("rules").getAsJsonArray();
assertThat(rules).hasSize(1);
var rule = rules.get(0).getAsJsonObject();
assertThat(rule.get("channelMask").getAsInt()).isEqualTo(1);
assertThat(rule.get("condition").getAsString()).isEqualTo("CONDITION_HIGH_THRESHOLD");
assertThat(rule.get("parameters").getAsJsonArray().get(0).getAsInt()).isEqualTo(500);
assertThat(rule.get("action").getAsString()).isEqualTo("ACTION_TRIGGER_TRANSMISSION");
}
@Test
void getEfentoConfiguration_emptyConfigProducesJsonWithDefaultValues() throws InvalidProtocolBufferException {
byte[] bytes = ConfigProtos.ProtoConfig.newBuilder().build().toByteArray();
var json = coapEfentoTransportResource.getEfentoConfiguration(bytes).getAsJsonObject();
// JsonFormat with includingDefaultValueFields prints all fields — verify a representative subset
assertThat(json.get("measurementPeriodBase").getAsLong()).isEqualTo(0);
assertThat(json.get("transmissionInterval").getAsLong()).isEqualTo(0);
assertThat(json.get("dataServerIp").getAsString()).isEmpty();
assertThat(json.get("deviceInfoRequest").getAsBoolean()).isFalse();
assertThat(json.get("cloudToken").getAsString()).isEmpty();
}
/**
* Builds a ProtoDeviceInfo with the minimum set of repeated fields required by getEfentoDeviceInfo:
* 22 memory_statistics, 34 modem parameters and 3 message_counters all set to 0.
*/
private static DeviceInfoProtos.ProtoDeviceInfo.Builder minimalDeviceInfo() {
List<Integer> zeroMemStats = java.util.Collections.nCopies(22, 0);
List<Integer> zeroModemParams = java.util.Collections.nCopies(34, 0);
return DeviceInfoProtos.ProtoDeviceInfo.newBuilder()
.setSerialNumber(integerToByteString(1234))
.setCloudToken("test_token")
.setSwVersion(0)
.addAllMemoryStatistics(zeroMemStats)
.setModem(DeviceInfoProtos.ProtoModem.newBuilder()
.setType(DeviceInfoProtos.ModemType.MODEM_TYPE_UNSPECIFIED)
.addAllParameters(zeroModemParams)
.build())
.setRuntimeInfo(DeviceInfoProtos.ProtoRuntime.newBuilder()
.setUpTime(0)
.addAllMessageCounters(List.of(0, 0, 0))
.build());
}
private static String formatDate(long seconds) {
SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z");
return sdf.format(new Date(TimeUnit.SECONDS.toMillis(seconds)));
}
public static ByteString integerToByteString(Integer intValue) {
@ -340,20 +907,17 @@ class CoapEfentoTransportResourceTest {
private void checkDefaultMeasurements(ProtoMeasurements incomingMeasurements,
List<CoapEfentoTransportResource.EfentoTelemetry> actualEfentoMeasurements,
long expectedMeasurementInterval,
boolean isBinarySensor) {
for (int i = 0; i < actualEfentoMeasurements.size(); i++) {
CoapEfentoTransportResource.EfentoTelemetry actualEfentoMeasurement = actualEfentoMeasurements.get(i);
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("serial").getAsString()).isEqualTo(CoapEfentoUtils.convertByteArrayToString(incomingMeasurements.getSerialNum().toByteArray()));
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("battery").getAsString()).isEqualTo(incomingMeasurements.getBatteryStatus() ? "ok" : "low");
MeasurementsProtos.ProtoChannel protoChannel = incomingMeasurements.getChannelsList().get(0);
long measuredAt = isBinarySensor ?
TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()) + Math.abs(TimeUnit.SECONDS.toMillis(protoChannel.getSampleOffsetsList().get(i))) - 1000 :
TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp() + i * expectedMeasurementInterval);
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("measured_at").getAsString()).isEqualTo(convertTimestampToUtcString(measuredAt));
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("next_transmission_at").getAsString()).isEqualTo(convertTimestampToUtcString(TimeUnit.SECONDS.toMillis(incomingMeasurements.getNextTransmissionAt())));
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("signal").getAsLong()).isEqualTo(incomingMeasurements.getSignal());
assertThat(actualEfentoMeasurement.getValues().getAsJsonObject().get("measurement_interval").getAsDouble()).isEqualTo(expectedMeasurementInterval);
long expectedMeasurementInterval) {
CoapEfentoTransportResource.EfentoTelemetry efentoTelemetry = actualEfentoMeasurements.stream()
.sorted(Comparator.comparing(CoapEfentoTransportResource.EfentoTelemetry::getTs))
.toList().get(0);
assertThat(efentoTelemetry.getValues().getAsJsonObject().get("serial").getAsString()).isEqualTo(CoapEfentoUtils.convertByteArrayToString(incomingMeasurements.getSerialNumber().toByteArray()));
assertThat(efentoTelemetry.getValues().getAsJsonObject().get("battery").getAsString()).isEqualTo(incomingMeasurements.getBatteryStatus() ? "ok" : "low");
assertThat(efentoTelemetry.getValues().getAsJsonObject().get("next_transmission_at").getAsString()).isEqualTo(convertTimestampToUtcString(TimeUnit.SECONDS.toMillis(incomingMeasurements.getNextTransmissionAt())));
assertThat(efentoTelemetry.getValues().getAsJsonObject().get("signal").getAsLong()).isEqualTo(incomingMeasurements.getSignal());
for (int i = 1; i < incomingMeasurements.getChannelsCount() + 1; i++) {
assertThat(efentoTelemetry.getValues().getAsJsonObject().get("measurement_interval_" + i).getAsDouble()).isEqualTo(expectedMeasurementInterval);
}
}

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -508,7 +508,9 @@ public class DefaultTransportService extends TransportActivityManager implements
@Override
public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionEventMsg msg, TransportServiceCallback<Void> callback) {
if (checkLimits(sessionInfo, msg, callback)) {
recordActivityInternal(sessionInfo);
if (msg.getEvent() != TransportProtos.SessionEvent.CLOSED) {
recordActivityInternal(sessionInfo);
}
sendToDeviceActor(sessionInfo, TransportToDeviceActorMsg.newBuilder().setSessionInfo(sessionInfo)
.setSessionEvent(msg).build(), callback);
}

58
common/transport/transport-api/src/test/java/org/thingsboard/server/common/transport/service/TransportActivityManagerTest.java

@ -26,12 +26,14 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.activity.ActivityReportCallback;
import org.thingsboard.server.common.transport.activity.ActivityState;
import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategy;
import org.thingsboard.server.common.transport.activity.strategy.ActivityStrategyType;
import org.thingsboard.server.common.transport.limits.TransportRateLimitService;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.UUID;
@ -41,7 +43,12 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@ -514,4 +521,55 @@ public class TransportActivityManagerTest {
verify(transportServiceMock, never()).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
}
@Test
void givenSessionClosedEvent_whenProcessingSessionEvent_thenShouldNotRecordActivity() {
// GIVEN - simulates the session-expiry path: session already removed from sessions map,
// then process(SESSION_CLOSED) is called. Activity must NOT be recorded to avoid active
// status update from false to true
var rateLimitServiceMock = mock(TransportRateLimitService.class);
ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock);
when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null);
TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
.setSessionIdMSB(SESSION_ID.getMostSignificantBits())
.setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
.build();
doCallRealMethod().when(transportServiceMock).process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
// WHEN
transportServiceMock.process(sessionInfo, SESSION_EVENT_MSG_CLOSED, null);
// THEN
verify(transportServiceMock, never()).onActivity(any(), any(), anyLong());
verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull());
}
@Test
void givenSessionOpenEvent_whenProcessingSessionEvent_thenShouldRecordActivity() {
// GIVEN
var rateLimitServiceMock = mock(TransportRateLimitService.class);
ReflectionTestUtils.setField(transportServiceMock, "rateLimitService", rateLimitServiceMock);
when(rateLimitServiceMock.checkLimits(any(), nullable(DeviceId.class), any(), anyInt(), anyBoolean())).thenReturn(null);
TransportProtos.SessionInfoProto sessionInfo = TransportProtos.SessionInfoProto.newBuilder()
.setSessionIdMSB(SESSION_ID.getMostSignificantBits())
.setSessionIdLSB(SESSION_ID.getLeastSignificantBits())
.build();
var sessionOpenMsg = TransportProtos.SessionEventMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.setEvent(TransportProtos.SessionEvent.OPEN)
.build();
when(transportServiceMock.toSessionId(sessionInfo)).thenReturn(SESSION_ID);
long expectedActivityTime = 100L;
when(transportServiceMock.getCurrentTimeMillis()).thenReturn(expectedActivityTime);
doCallRealMethod().when(transportServiceMock).process(sessionInfo, sessionOpenMsg, null);
// WHEN
transportServiceMock.process(sessionInfo, sessionOpenMsg, null);
// THEN
verify(transportServiceMock).onActivity(SESSION_ID, sessionInfo, expectedActivityTime);
verify(transportServiceMock).sendToDeviceActor(eq(sessionInfo), any(), isNull());
}
}

13
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmCommentService.java

@ -48,13 +48,13 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
@Override
public AlarmComment createOrUpdateAlarmComment(TenantId tenantId, AlarmComment alarmComment) {
alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
AlarmComment oldAlarmComment = alarmCommentDataValidator.validate(alarmComment, c -> tenantId);
boolean isCreated = alarmComment.getId() == null;
AlarmComment result;
if (isCreated) {
result = createAlarmComment(tenantId, alarmComment);
} else {
result = updateAlarmComment(tenantId, alarmComment);
result = updateAlarmComment(tenantId, alarmComment, oldAlarmComment);
}
if (result != null) {
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(result)
@ -101,18 +101,17 @@ public class BaseAlarmCommentService extends AbstractEntityService implements Al
return alarmCommentDao.save(tenantId, alarmComment);
}
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment) {
private AlarmComment updateAlarmComment(TenantId tenantId, AlarmComment newAlarmComment, AlarmComment oldAlarmComment) {
log.debug("Update Alarm comment : {}", newAlarmComment);
AlarmComment existing = alarmCommentDao.findAlarmCommentById(tenantId, newAlarmComment.getId().getId());
if (existing != null) {
if (oldAlarmComment != null) {
if (newAlarmComment.getComment() != null) {
JsonNode comment = newAlarmComment.getComment();
((ObjectNode) comment).put("edited", "true");
((ObjectNode) comment).put("editedOn", System.currentTimeMillis());
existing.setComment(comment);
oldAlarmComment.setComment(comment);
}
return alarmCommentDao.save(tenantId, existing);
return alarmCommentDao.save(tenantId, oldAlarmComment);
}
return null;
}

10
dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java

@ -15,15 +15,9 @@
*/
package org.thingsboard.server.dao.rule;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChainDetails;
import org.thingsboard.server.dao.Dao;
import java.util.UUID;
public interface RuleChainDetailsDao {
RuleChainDetails findById(TenantId tenantId, UUID id);
RuleChainDetails save(TenantId tenantId, RuleChainDetails ruleChainDetails);
public interface RuleChainDetailsDao extends Dao<RuleChainDetails> {
}

22
dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java

@ -18,14 +18,18 @@ package org.thingsboard.server.dao.service.validator;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.exception.DataValidationException;
import org.thingsboard.server.dao.alarm.AlarmCommentDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.exception.DataValidationException;
@Component
@AllArgsConstructor
public class AlarmCommentDataValidator extends DataValidator<AlarmComment> {
private final AlarmCommentDao alarmCommentDao;
@Override
protected void validateDataImpl(TenantId tenantId, AlarmComment alarmComment) {
if (alarmComment.getComment() == null) {
@ -35,4 +39,20 @@ public class AlarmCommentDataValidator extends DataValidator<AlarmComment> {
throw new DataValidationException("Alarm id should be specified!");
}
}
@Override
protected AlarmComment validateUpdate(TenantId tenantId, AlarmComment alarmComment) {
AlarmComment oldAlarmComment = null;
if (alarmComment.getId() != null) {
oldAlarmComment = alarmCommentDao.findAlarmCommentById(tenantId, alarmComment.getId().getId());
if (oldAlarmComment == null) {
throw new DataValidationException("Can't update non existing alarm comment!");
}
if (oldAlarmComment.getType() == AlarmCommentType.SYSTEM) {
throw new DataValidationException("System alarm comment can't be updated!");
}
}
return oldAlarmComment;
}
}

16
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java

@ -23,12 +23,14 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChainDetails;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity;
import org.thingsboard.server.dao.rule.RuleChainDetailsDao;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.exception.DataValidationException;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@ -40,9 +42,19 @@ public class JpaRuleChainDetailsDao extends JpaAbstractDao<RuleChainDetailsEntit
private final RuleChainDetailsRepository ruleChainDetailsRepository;
@Override
public RuleChainDetails save(TenantId tenantId, RuleChainDetails ruleChainDetails) {
public RuleChainDetails findById(TenantId tenantId, UUID key) {
Optional<RuleChainDetailsEntity> entity = getRepository().findById(key);
// Detaching to avoid stale version conflict with RuleChainEntity which maps to the same table.
// Without detach, a loaded RuleChainDetailsEntity stays in the persistence context and becomes stale
// when the same row is updated via RuleChainEntity (e.g. during rule chain import with circular references).
entity.ifPresent(e -> getEntityManager().detach(e));
return DaoUtil.getData(entity);
}
@Override
protected RuleChainDetailsEntity doSave(RuleChainDetailsEntity entity, boolean isNew, boolean flush) {
try {
return super.save(tenantId, ruleChainDetails);
return super.doSave(entity, isNew, flush);
} catch (Exception e) {
String rootMsg = ExceptionUtils.getRootCauseMessage(e);
if (StringUtils.contains(rootMsg, "value too long")) {

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

Loading…
Cancel
Save