Browse Source

Merge remote-tracking branch 'thingsboard-ce/master' into feat/outline-inputs

pull/15357/head
Maksym Tsymbarov 4 months ago
parent
commit
313464cf28
  1. 88
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  2. 259
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  3. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  4. 118
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  5. 350
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  6. 4
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  7. 8
      application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java
  8. 7
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  9. 14
      application/src/main/java/org/thingsboard/server/controller/RuleEngineController.java
  10. 15
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  11. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  12. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  13. 178
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  14. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  15. 420
      application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  18. 7
      common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java
  19. 7
      common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java
  20. 166
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java
  21. 39
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java
  22. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  23. 10
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDetailsDao.java
  24. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDetailsDao.java
  25. 2
      msa/js-executor/docker/Dockerfile
  26. 2
      msa/js-executor/pom.xml
  27. 2
      msa/web-ui/docker/Dockerfile
  28. 2
      msa/web-ui/pom.xml
  29. 22
      pom.xml
  30. 2
      ui-ngx/pom.xml
  31. 73
      ui-ngx/src/app/core/http/alarm-rules.service.ts
  32. 24
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  33. 105
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  34. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  35. 35
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts
  36. 6
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  37. 13
      ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts
  38. 6
      ui-ngx/src/app/shared/models/calculated-field.models.ts

88
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;
@ -294,6 +298,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();
@ -355,7 +372,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() {
@ -407,6 +425,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 +622,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

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);
}

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);
}

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);

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);
}
}

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();
}
}

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());
}
}

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;
}

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> {
}

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")) {

2
msa/js-executor/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM thingsboard/node:22.18.0-bookworm-slim
FROM thingsboard/node:22.22.2-bookworm-slim
ENV NODE_ENV production
ENV DOCKER_MODE true

2
msa/js-executor/pom.xml

@ -70,7 +70,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v22.18.0</nodeVersion>
<nodeVersion>v22.22.2</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>

2
msa/web-ui/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM thingsboard/node:22.18.0-bookworm-slim
FROM thingsboard/node:22.22.2-bookworm-slim
ENV NODE_ENV production
ENV DOCKER_MODE true

2
msa/web-ui/pom.xml

@ -79,7 +79,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v22.18.0</nodeVersion>
<nodeVersion>v22.22.2</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>

22
pom.xml

@ -63,9 +63,7 @@
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<thingsboard.client.version>4.4.0-SNAPSHOT</thingsboard.client.version>
<spring-boot.version>3.5.12</spring-boot.version>
<jackson-bom.version>2.21.1</jackson-bom.version> <!-- to fix GHSA-72hv-8253-57qq. TODO: remove when fixed in spring-boot-dependencies -->
<netty.version>4.1.132.Final</netty.version> <!-- to fix CVE-2026-33870 and CVE-2026-33871. TODO: remove when fixed in spring-boot-dependencies -->
<spring-boot.version>3.5.13</spring-boot.version>
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jjwt.version>0.12.5</jjwt.version>
<rat.version>0.10</rat.version> <!-- unused -->
@ -996,24 +994,6 @@
<dependencyManagement>
<dependencies>
<!-- Temporary jackson-bom version override -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- End of jackson-bom version override -->
<!-- Temporary netty-bom version override -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
<version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- End of netty-bom version override -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>

2
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v22.18.0</nodeVersion>
<nodeVersion>v22.22.2</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</execution>

73
ui-ngx/src/app/core/http/alarm-rules.service.ts

@ -0,0 +1,73 @@
///
/// 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.
///
import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, defaultHttpOptionsFromParams, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import {
CalculatedFieldAlarmRule,
CalculatedFieldAlarmRuleInfo,
CalculatedFieldsQuery,
CalculatedFieldTestScriptInputParams
} from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
import { CalculatedFieldEventBody } from '@shared/models/event.models';
@Injectable({
providedIn: 'root'
})
export class AlarmRulesService {
constructor(
private http: HttpClient
) { }
public getAlarmRuleById(alarmRuleId: string, config?: RequestConfig): Observable<CalculatedFieldAlarmRule> {
return this.http.get<CalculatedFieldAlarmRule>(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config));
}
public saveAlarmRule(alarmRule: CalculatedFieldAlarmRule, config?: RequestConfig): Observable<CalculatedFieldAlarmRule> {
return this.http.post<CalculatedFieldAlarmRule>('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config));
}
public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable<boolean> {
return this.http.delete<boolean>(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config));
}
public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable<PageData<CalculatedFieldAlarmRuleInfo>> {
return this.http.get<PageData<CalculatedFieldAlarmRuleInfo>>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config));
}
public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedFieldAlarmRule>> {
return this.http.get<PageData<CalculatedFieldAlarmRule>>(`/api/alarm/rules/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
return this.http.post<EntityTestScriptResult>('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config));
}
public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable<CalculatedFieldEventBody> {
return this.http.get<CalculatedFieldEventBody>(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config));
}
public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable<PageData<string>> {
return this.http.get<PageData<string>>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
}

24
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -21,11 +21,15 @@ import { AppState } from '@core/core.state';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
import {
CalculatedFieldAlarmRule,
CalculatedFieldArgument,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { AlarmRulesService } from '@core/http/alarm-rules.service';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
@ -49,13 +53,13 @@ import { AssetInfo } from '@shared/models/asset.models';
import { DeviceInfo } from '@shared/models/device.models';
export interface AlarmRuleDialogData {
value?: CalculatedField;
value?: CalculatedFieldAlarmRule;
buttonTitle: string;
entityId: EntityId;
tenantId: string;
entityName?: string;
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedFieldAlarmRule) => void>;
isDirty?: boolean;
getTestScriptDialogFn: AlarmRuleTestScriptFn,
}
@ -67,7 +71,7 @@ export interface AlarmRuleDialogData {
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> {
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedFieldAlarmRule> {
@ViewChild('entitySelect') entitySelect!: EntitySelectComponent;
@ -96,8 +100,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedFieldAlarmRule>,
private alarmRulesService: AlarmRulesService,
private destroyRef: DestroyRef,
private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef);
@ -159,8 +163,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.configFormGroup.patchValue({clearRule: clearAlarmRule});
}
get fromGroupValue(): CalculatedField {
return deepTrim(this.fieldFormGroup.value as CalculatedField);
get fromGroupValue(): CalculatedFieldAlarmRule {
return deepTrim(this.fieldFormGroup.value as CalculatedFieldAlarmRule);
}
cancel(): void {
@ -173,7 +177,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue};
alarmRule.configuration.type = CalculatedFieldType.ALARM;
this.calculatedFieldsService.saveCalculatedField(alarmRule)
this.alarmRulesService.saveAlarmRule(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: calculatedField => this.dialogRef.close(calculatedField),

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

@ -21,7 +21,7 @@ import {
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
@ -35,15 +35,14 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { AlarmRulesService } from '@core/http/alarm-rules.service';
import { catchError, filter, first, switchMap, tap } from 'rxjs/operators';
import {
ArgumentEntityType,
ArgumentType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldAlarmRuleInfo,
CalculatedFieldEventArguments,
CalculatedFieldInfo,
CalculatedFieldsQuery,
CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
@ -72,7 +71,7 @@ import { Router } from '@angular/router';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { AlarmRulesComponent } from '@home/components/alarm-rules/alarm-rules.component';
type AlarmRuleTableEntity = CalculatedField | CalculatedFieldInfo;
export type AlarmRuleTableEntity = CalculatedFieldAlarmRule | CalculatedFieldAlarmRuleInfo;
export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntity> {
@ -84,7 +83,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
alarmRuleFilterConfig: CalculatedFieldsQuery;
constructor(private calculatedFieldsService: CalculatedFieldsService,
constructor(private alarmRulesService: AlarmRulesService,
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe,
@ -128,14 +127,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedAlarmDialog.bind(this);
this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id);
this.saveEntity = (alarmRule) => this.calculatedFieldsService.saveCalculatedField(alarmRule);
this.loadEntity = id => this.alarmRulesService.getAlarmRuleById(id.id);
this.saveEntity = (alarmRule) => this.alarmRulesService.saveAlarmRule(alarmRule);
this.deleteEntityTitle = (field) => this.translate.instant('alarm-rule.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.deleteEntity = id => this.alarmRulesService.deleteAlarmRule(id.id);
this.onEntityAction = action => this.onCFAction(action);
@ -159,9 +158,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
if (this.pageMode) {
this.columns.push(new EntityTableColumn<CalculatedFieldInfo>('entityType', 'entity.entity-type', '10%',
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRuleInfo>('entityType', 'entity.entity-type', '10%',
entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
this.columns.push(new EntityLinkTableColumn<CalculatedFieldInfo>('entityName', 'entity.entity', '33%',
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRuleInfo>('entityName', 'entity.entity', '33%',
entity => this.utilsService.customTranslation(entity.entityName, entity.entityName),
entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
}
@ -216,8 +215,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<AlarmRuleTableEntity>> {
return this.pageMode ?
this.calculatedFieldsService.getCalculatedFields(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) :
this.calculatedFieldsService.getCalculatedFieldsByEntityId(this.entityId, pageLink, CalculatedFieldType.ALARM);
this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) :
this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink);
}
onOpenDebugConfig($event: Event, calculatedField: AlarmRuleTableEntity): void {
@ -260,7 +259,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
const copyCalculatedAlarmRule = deepClone(calculatedField);
if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
delete (copyCalculatedAlarmRule as CalculatedFieldInfo).entityName;
delete (copyCalculatedAlarmRule as CalculatedFieldAlarmRuleInfo).entityName;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty)
@ -271,10 +270,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
});
}
private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedFieldAlarmRule> {
const entityId = this.entityId || value?.entityId;
const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName;
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedField>(AlarmRuleDialogComponent, {
const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName;
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedFieldAlarmRule>(AlarmRuleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -353,14 +352,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
}),
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
switchMap(calculatedField => this.alarmRulesService.saveAlarmRule(calculatedField)),
filter(Boolean),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => this.updateData());
}
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
private updateImportedCalculatedField(calculatedField: CalculatedFieldAlarmRule): CalculatedFieldAlarmRule {
if (calculatedField.type === CalculatedFieldType.ALARM) {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key];
@ -375,48 +374,44 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
}
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
this.alarmRulesService.getAlarmRuleById(id).pipe(
switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData());
}
private getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if (calculatedField.type === CalculatedFieldType.ALARM) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key)
? {...argumentsObj[key], type}
: type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()};
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
getTestScriptDialog(calculatedField: AlarmRuleTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key)
? {...argumentsObj[key], type}
: type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()};
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField(null, {
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField(null, {
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
}
}),
);
} else {
return of(null);
}
}),
);
}
private openCalculatedField($event: Event, entity: AlarmRuleTableEntity) {

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

@ -30,7 +30,7 @@ import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { AlarmRulesService } from '@core/http/alarm-rules.service';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
@ -59,7 +59,7 @@ export class AlarmRulesTableComponent {
pageMode: boolean = false;
constructor(private calculatedFieldsService: CalculatedFieldsService,
constructor(private alarmRulesService: AlarmRulesService,
private translate: TranslateService,
private dialog: MatDialog,
private store: Store<AppState>,
@ -77,7 +77,7 @@ export class AlarmRulesTableComponent {
effect(() => {
if (this.active() || this.pageMode) {
this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService,
this.alarmRulesService,
this.translate,
this.dialog,
this.datePipe,

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

@ -22,19 +22,19 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import {
CalculatedFieldAlarmRuleConfiguration,
CalculatedFieldAlarmRuleInfo,
CalculatedFieldArgument,
CalculatedFieldConfiguration,
CalculatedFieldInfo,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { EntityId } from '@shared/models/id/entity-id';
import { BaseData } from '@shared/models/base-data';
import { Observable } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
import type {
AlarmRulesTableConfig,
AlarmRuleTableEntity
} from '@home/components/alarm-rules/alarm-rules-table-config';
import { TenantId } from '@shared/models/id/tenant-id';
import { StringItemsOption } from '@shared/components/string-items-list.component';
import { RelationTypes } from '@shared/models/relation.models';
@ -56,7 +56,7 @@ import { EntityService } from '@core/http/entity.service';
styleUrls: ['./alarm-rule-dialog.component.scss'],
standalone: false
})
export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEntity> {
export class AlarmRulesComponent extends EntityComponent<AlarmRuleTableEntity> {
@Input()
standalone = false;
@ -75,8 +75,8 @@ export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEn
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Inject('entity') protected entityValue: CalculatedFieldInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig,
@Inject('entity') protected entityValue: CalculatedFieldAlarmRuleInfo,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: AlarmRulesTableConfig,
protected fb: FormBuilder,
protected cd: ChangeDetectorRef,
private entityService: EntityService) {
@ -93,21 +93,14 @@ export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEn
additionalDebugActionConfig = {
...this.entitiesTableConfig.additionalDebugActionConfig,
action: () => this.entitiesTableConfig.additionalDebugActionConfig.action(
{ id: this.entity.id, ...this.entityFormValue() }, false,
(expression) => {
if (expression) {
this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression});
this.entityForm.get('configuration').markAsDirty();
}
}),
action: () => this.entitiesTableConfig.additionalDebugActionConfig.action({id: this.entity.id, ...this.entityFormValue()})
};
get entityId(): EntityId {
return this.entityForm.get('entityId').value;
}
get entitiesTableConfig(): CalculatedFieldsTableConfig {
get entitiesTableConfig(): AlarmRulesTableConfig {
return this.entitiesTableConfigValue;
}
@ -115,12 +108,12 @@ export class AlarmRulesComponent extends EntityComponent<CalculatedFieldsTableEn
this.entityName = entity?.name;
}
buildForm(_entity?: CalculatedFieldInfo): FormGroup {
buildForm(_entity?: CalculatedFieldAlarmRuleInfo): FormGroup {
return inject(CalculatedFieldFormService).buildAlarmRuleForm();
}
updateForm(entity: CalculatedFieldInfo) {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {};
updateForm(entity: CalculatedFieldAlarmRuleInfo) {
const { configuration = {} as CalculatedFieldAlarmRuleConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {};
this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false});
if (!entityId) {
this.entityForm.get('configuration').disable({emitEvent: false});

6
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -26,7 +26,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { AlarmRulesService } from '@core/http/alarm-rules.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -40,7 +40,7 @@ import { AlarmRulesTableConfig } from '@home/components/alarm-rules/alarm-rules-
export const AlarmRulesTableConfigResolver: ResolveFn<AlarmRulesTableConfig> =
(_route: ActivatedRouteSnapshot,
_state: RouterStateSnapshot,
calculatedFieldsService = inject(CalculatedFieldsService),
alarmRulesService = inject(AlarmRulesService),
translate = inject(TranslateService),
dialog = inject(MatDialog),
store = inject(Store<AppState>),
@ -52,7 +52,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn<AlarmRulesTableConfig> =
router = inject(Router),
) => {
return new AlarmRulesTableConfig(
calculatedFieldsService,
alarmRulesService,
translate,
dialog,
datePipe,

13
ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts

@ -18,9 +18,9 @@ import { Component } from '@angular/core';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models';
import type {
CalculatedFieldsTableConfig,
CalculatedFieldsTableEntity
} from '@home/components/calculated-fields/calculated-fields-table-config';
AlarmRulesTableConfig,
AlarmRuleTableEntity
} from '@home/components/alarm-rules/alarm-rules-table-config';
import { debugCfActionEnabled } from '@shared/models/calculated-field.models';
@Component({
@ -29,7 +29,7 @@ import { debugCfActionEnabled } from '@shared/models/calculated-field.models';
styleUrls: [],
standalone: false
})
export class AlarmRulesTabsComponent extends EntityTabsComponent<CalculatedFieldsTableEntity> {
export class AlarmRulesTabsComponent extends EntityTabsComponent<AlarmRuleTableEntity> {
readonly DebugEventType = DebugEventType;
readonly EventType = EventType;
@ -43,8 +43,7 @@ export class AlarmRulesTabsComponent extends EntityTabsComponent<CalculatedField
};
onDebugEventSelected(event: CalculatedFieldEventBody) {
(this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments))
.subscribe((expression) => {
});
(this.entitiesTableConfig as AlarmRulesTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments))
.subscribe((_expression) => { });
};
}

6
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -80,7 +80,9 @@ export type CalculatedField =
| CalculatedFieldRelatedEntityAggregation
| CalculatedFieldAlarmRule;
export type CalculatedFieldInfo = CalculatedField & {entityName: string};
export type WithCalculatedFieldInfo<T> = T & {entityName: string};
export type CalculatedFieldInfo = WithCalculatedFieldInfo<CalculatedField>;
export type CalculatedFieldAlarmRuleInfo = WithCalculatedFieldInfo<CalculatedFieldAlarmRule>;
export enum CalculatedFieldType {
SIMPLE = 'SIMPLE',
@ -192,7 +194,7 @@ interface BasePropagationConfiguration {
output: CalculatedFieldOutput;
}
interface CalculatedFieldAlarmRuleConfiguration {
export interface CalculatedFieldAlarmRuleConfiguration {
type: CalculatedFieldType.ALARM;
arguments: Record<string, CalculatedFieldArgument>;
createRules: Record<AlarmSeverity, AlarmRule>;

Loading…
Cancel
Save