Browse Source

Merge pull request #15326 from thingsboard/feature/alarm-rules-api

Added dedicated alarm rules API
pull/15369/head
Viacheslav Klimov 6 months ago
committed by GitHub
parent
commit
5527382cbb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 259
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  2. 12
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 118
      application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java
  4. 350
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  5. 92
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/DefaultTbCalculatedFieldService.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/cf/TbCalculatedFieldService.java
  7. 178
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  8. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  9. 420
      application/src/test/java/org/thingsboard/server/controller/AlarmRuleControllerTest.java
  10. 166
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java
  11. 39
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java
  12. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileData.java
  13. 73
      ui-ngx/src/app/core/http/alarm-rules.service.ts
  14. 24
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  15. 105
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  16. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  17. 35
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts
  18. 6
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  19. 13
      ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts
  20. 6
      ui-ngx/src/app/shared/models/calculated-field.models.ts

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

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

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

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