Browse Source

Add separate alarm rules API

pull/15326/head
Viacheslav Klimov 5 months ago
parent
commit
5768bbefe1
Failed to extract signature
  1. 365
      application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
  2. 178
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  3. 5
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  4. 165
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java
  5. 39
      common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java
  6. 985
      docs/superpowers/plans/2026-03-30-alarm-rule-controller.md
  7. 144
      docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md
  8. 73
      ui-ngx/src/app/core/http/alarm-rules.service.ts
  9. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  10. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  11. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  12. 6
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

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

@ -0,0 +1,365 @@
/**
* 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 io.swagger.v3.oas.annotations.Parameter;
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.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.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.AlarmRuleDefinition;
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.cf.configuration.CalculatedFieldConfiguration;
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.service.security.permission.Operation;
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 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 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
@Slf4j
public class AlarmRuleController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
public static final String ALARM_RULE_ID = "alarmRuleId";
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
+ "{\n" +
" \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" +
" \"arguments\": {\n" +
" \"temperature\": {\n" +
" \"type\": \"TS_ROLLING\",\n" +
" \"timeWindow\": {\n" +
" \"startTs\": 1739775630002,\n" +
" \"endTs\": 65432211,\n" +
" \"limit\": 5\n" +
" },\n" +
" \"values\": [\n" +
" { \"ts\": 1739775639851, \"value\": 23 },\n" +
" { \"ts\": 1739775664561, \"value\": 43 },\n" +
" { \"ts\": 1739775713079, \"value\": 15 },\n" +
" { \"ts\": 1739775999522, \"value\": 34 },\n" +
" { \"ts\": 1739776228452, \"value\": 22 }\n" +
" ]\n" +
" },\n" +
" \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\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);
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."
)
@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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkNotNull(calculatedField);
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."
)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@GetMapping(value = "/alarm/rule/{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.")
@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<CalculatedFieldType> types = EnumSet.of(CalculatedFieldType.ALARM);
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream()
.filter(entry -> CollectionUtils.containsAny(entry.getValue(), types))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
} else {
entityTypes = EnumSet.of(entityType);
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.types(types)
.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.")
@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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
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 Script expression",
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 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 entityTypeVal = referencedEntityId.getEntityType();
switch (entityTypeVal) {
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityTypeVal + "' for referenced entities.");
}
}
}
}

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

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

@ -0,0 +1,165 @@
/**
* 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.EntityType;
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.HasVersion;
import org.thingsboard.server.common.data.HasTenantId;
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")
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 EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
}
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());
def.setConfiguration((AlarmCalculatedFieldConfiguration) cf.getConfiguration());
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());
}
}

985
docs/superpowers/plans/2026-03-30-alarm-rule-controller.md

@ -0,0 +1,985 @@
# Alarm Rule Controller Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Create a dedicated AlarmRuleController REST API that wraps the CalculatedField service layer, hiding the CF implementation detail from alarm rule API consumers.
**Architecture:** Thin controller wrapper. New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs wrap `CalculatedField`/`CalculatedFieldInfo`. `AlarmRuleController` delegates to `TbCalculatedFieldService` and `CalculatedFieldService` (DAO). UI gets a new `AlarmRulesService` pointing at the new endpoints. `AlarmRulesTest` switches to the new API.
**Tech Stack:** Java 17+, Spring Boot, Angular 17+, JUnit 4 / Spring MockMvc
---
## File Structure
**Backend (new):**
- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java` — DTO wrapping CalculatedField without `type`
- `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java` — extends AlarmRuleDefinition, adds `entityName`
- `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java` — REST controller
**Backend (modify):**
- `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652` — add `saveAlarmRule()` helper alongside existing `saveCalculatedField()`
- `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java` — switch from CF API to alarm rule API
**Frontend (new):**
- `ui-ngx/src/app/core/http/alarm-rules.service.ts` — Angular HTTP service for alarm rule endpoints
**Frontend (modify):**
- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts` — switch from `CalculatedFieldsService` to `AlarmRulesService`
- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts` — inject `AlarmRulesService` instead of `CalculatedFieldsService`
- `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts` — use `AlarmRulesService` for all CRUD operations
---
### Task 1: Create AlarmRuleDefinition DTO
**Files:**
- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java`
- [ ] **Step 1: Create AlarmRuleDefinition class**
```java
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.HasVersion;
import org.thingsboard.server.common.data.TenantEntity;
import org.thingsboard.server.common.data.EntityType;
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, TenantEntity, HasVersion, HasDebugSettings, HasAdditionalInfo {
private TenantId tenantId;
private EntityId entityId;
@NoXss
@Length(fieldName = "name")
@Schema(description = "Alarm type name.")
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")
private JsonNode additionalInfo;
public AlarmRuleDefinition() {}
public AlarmRuleDefinition(CalculatedFieldId id) {
super(id);
}
public AlarmRuleDefinition(AlarmRuleDefinition other) {
super(other);
this.tenantId = other.tenantId;
this.entityId = other.entityId;
this.name = other.name;
this.debugMode = other.debugMode;
this.debugSettings = other.debugSettings;
this.configurationVersion = other.configurationVersion;
this.configuration = other.configuration;
this.version = other.version;
this.additionalInfo = other.additionalInfo;
}
@Schema(description = "JSON object with the Alarm Rule Id.")
@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();
}
@JsonIgnore
public boolean isDebugMode() {
return debugMode;
}
@JsonSetter
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
@Override
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
}
public CalculatedField toCalculatedField() {
CalculatedField cf = new CalculatedField();
cf.setId(this.getId());
cf.setCreatedTime(this.getCreatedTime());
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());
def.setConfiguration((AlarmCalculatedFieldConfiguration) cf.getConfiguration());
def.setVersion(cf.getVersion());
def.setAdditionalInfo(cf.getAdditionalInfo());
return def;
}
}
```
- [ ] **Step 2: Verify compilation**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5`
Expected: BUILD SUCCESS
- [ ] **Step 3: Commit**
```bash
git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java
git commit -m "Add AlarmRuleDefinition DTO"
```
---
### Task 2: Create AlarmRuleDefinitionInfo DTO
**Files:**
- Create: `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java`
- [ ] **Step 1: Create AlarmRuleDefinitionInfo class**
```java
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) {
AlarmRuleDefinitionInfo info = new AlarmRuleDefinitionInfo();
AlarmRuleDefinition def = AlarmRuleDefinition.fromCalculatedField(cfi);
info.setId(def.getId());
info.setCreatedTime(def.getCreatedTime());
info.setTenantId(def.getTenantId());
info.setEntityId(def.getEntityId());
info.setName(def.getName());
info.setDebugMode(def.isDebugMode());
info.setDebugSettings(def.getDebugSettings());
info.setConfigurationVersion(def.getConfigurationVersion());
info.setConfiguration(def.getConfiguration());
info.setVersion(def.getVersion());
info.setAdditionalInfo(def.getAdditionalInfo());
info.setEntityName(cfi.getEntityName());
return info;
}
}
```
- [ ] **Step 2: Verify compilation**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl common/data -am -q -DskipTests 2>&1 | tail -5`
Expected: BUILD SUCCESS
- [ ] **Step 3: Commit**
```bash
git add common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java
git commit -m "Add AlarmRuleDefinitionInfo DTO"
```
---
### Task 3: Create AlarmRuleController
**Files:**
- Create: `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java`
- [ ] **Step 1: Create the controller with all endpoints**
```java
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
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 lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.http.HttpStatus;
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.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.AlarmRuleDefinition;
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.common.data.permission.Operation;
import org.thingsboard.server.common.data.permission.Resource;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
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 java.util.stream.Collectors;
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
@Slf4j
public class AlarmRuleController extends BaseController {
private final TbCalculatedFieldService tbCalculatedFieldService;
private final EventService eventService;
private final TbelInvokeService tbelInvokeService;
private static final String ALARM_RULE_ID = "alarmRuleId";
private 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
+ "{\n" +
" \"expression\": \"var temp = 0; foreach(element: temperature.values) {temp += element.value;} var avgTemperature = temp / temperature.values.size(); var adjustedTemperature = avgTemperature + 0.1 * humidity.value; return {\\\"adjustedTemperature\\\": adjustedTemperature};\",\n" +
" \"arguments\": {\n" +
" \"temperature\": {\n" +
" \"type\": \"TS_ROLLING\",\n" +
" \"timeWindow\": {\n" +
" \"startTs\": 1739775630002,\n" +
" \"endTs\": 65432211,\n" +
" \"limit\": 5\n" +
" },\n" +
" \"values\": [\n" +
" { \"ts\": 1739775639851, \"value\": 23 },\n" +
" { \"ts\": 1739775664561, \"value\": 43 },\n" +
" { \"ts\": 1739775713079, \"value\": 15 },\n" +
" { \"ts\": 1739775999522, \"value\": 34 },\n" +
" { \"ts\": 1739776228452, \"value\": 22 }\n" +
" ]\n" +
" },\n" +
" \"humidity\": { \"type\": \"SINGLE_VALUE\", \"ts\": 1739776478057, \"value\": 23 }\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, 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. " +
"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());
CalculatedField calculatedField = alarmRuleDefinition.toCalculatedField();
checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD);
checkReferencedEntities(calculatedField);
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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
checkNotNull(calculatedField);
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("/alarm/rule/{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 = "Filter by alarm rule name.") @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("/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.") @RequestParam(required = false) EntityType entityType,
@Parameter(description = "Entities filter.") @RequestParam(required = false) Set<UUID> entities,
@Parameter(description = "Filter by alarm rule name.") @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<CalculatedFieldType> types = EnumSet.of(CalculatedFieldType.ALARM);
Set<EntityType> entityTypes;
if (entityType == null) {
entityTypes = CalculatedField.SUPPORTED_ENTITIES.entrySet().stream()
.filter(entry -> entry.getValue().contains(CalculatedFieldType.ALARM))
.map(Map.Entry::getKey)
.filter(t -> {
try {
return accessControlService.hasPermission(user, Resource.resourceFromEntityType(t), Operation.READ_CALCULATED_FIELD);
} catch (ThingsboardException e) {
return false;
}
})
.collect(Collectors.toSet());
} else {
accessControlService.checkPermission(user, Resource.resourceFromEntityType(entityType), Operation.READ_CALCULATED_FIELD);
entityTypes = EnumSet.of(entityType);
}
CalculatedFieldFilter filter = CalculatedFieldFilter.builder()
.types(types)
.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("/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 = "Filter by alarm rule name.") @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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
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 = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser());
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 Script expression",
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 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(CalculatedField calculatedField) throws ThingsboardException {
Set<EntityId> referencedEntityIds = calculatedField.getConfiguration().getReferencedEntities();
for (EntityId referencedEntityId : referencedEntityIds) {
EntityType refEntityType = referencedEntityId.getEntityType();
switch (refEntityType) {
case TENANT -> {
return;
}
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
default -> throw new IllegalArgumentException("Alarm rules do not support '" + refEntityType + "' for referenced entities.");
}
}
}
}
```
- [ ] **Step 2: Check that `PageData.mapData` exists**
The controller uses `result.mapData(...)` to convert page results. Verify this method exists:
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && grep -n 'mapData' common/data/src/main/java/org/thingsboard/server/common/data/page/PageData.java`
If `mapData` doesn't exist, replace with manual conversion:
```java
new PageData<>(result.getData().stream().map(AlarmRuleDefinition::fromCalculatedField).toList(), result.getTotalPages(), result.getTotalElements(), result.hasNext())
```
- [ ] **Step 3: Verify compilation**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn compile -pl application -am -q -DskipTests 2>&1 | tail -10`
Expected: BUILD SUCCESS
- [ ] **Step 4: Commit**
```bash
git add application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java
git commit -m "Add AlarmRuleController with all endpoints"
```
---
### Task 4: Update AlarmRulesTest to use new API
**Files:**
- Modify: `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java:1652`
- Modify: `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`
- [ ] **Step 1: Add `saveAlarmRule` helper to AbstractWebTest**
In `application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java`, after the existing `saveCalculatedField` method at line 1652-1654, add:
```java
protected AlarmRuleDefinition saveAlarmRule(AlarmRuleDefinition alarmRule) {
return doPost("/api/alarm/rule", alarmRule, AlarmRuleDefinition.class);
}
```
Also add the necessary import at the top of AbstractWebTest.java:
```java
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
```
- [ ] **Step 2: Update AlarmRulesTest imports**
In `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`, add the import:
```java
import org.thingsboard.server.common.data.cf.AlarmRuleDefinition;
```
- [ ] **Step 3: Update `createAlarmCf` to `createAlarmRule` using new DTO**
Replace the `createAlarmCf` method (starting at line 955) with:
```java
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;
AlarmRuleDefinition alarmRuleDefinition = new AlarmRuleDefinition();
alarmRuleDefinition.setEntityId(entityId);
alarmRuleDefinition.setName(alarmType);
AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration();
configuration.setArguments(arguments);
configuration.setCreateRules(createRules);
configuration.setClearRule(clearRule);
alarmRuleDefinition.setConfiguration(configuration);
alarmRuleDefinition.setDebugSettings(DebugSettings.all());
if (modifier.length > 0) {
modifier[0].accept(configuration);
}
AlarmRuleDefinition saved = saveAlarmRule(alarmRuleDefinition);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDebugEvents(saved.getId(), 1),
events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return saved;
}
```
- [ ] **Step 4: Update all test methods that call `createAlarmCf`**
Search and replace all occurrences of `createAlarmCf` with `createAlarmRule` in AlarmRulesTest.java. Also update the local variable type from `CalculatedField` to `AlarmRuleDefinition` wherever the return value of `createAlarmRule` is stored. For example, change:
```java
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", arguments, createRules, clearRule);
```
to:
```java
AlarmRuleDefinition alarmRule = createAlarmRule(deviceId, "High Temperature Alarm", arguments, createRules, clearRule);
```
Then update references from `calculatedField` to `alarmRule` in the assertion calls like `checkAlarmResult(calculatedField, ...)``checkAlarmResult(alarmRule, ...)`.
- [ ] **Step 5: Update `checkAlarmResult` to accept AlarmRuleDefinition**
The `checkAlarmResult` method currently takes `CalculatedField`. Update it to accept `AlarmRuleDefinition`:
```java
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer<TbAlarmResult> assertion) {
return checkAlarmResult(alarmRule, assertion, null);
}
private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer<TbAlarmResult> assertion, Predicate<TbAlarmResult> waitFor) {
TbAlarmResult alarmResult = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.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(alarmRule.getName());
return alarmResult;
}
```
- [ ] **Step 6: Remove unused `CalculatedField` and `CalculatedFieldType` imports if no longer referenced**
After all changes, remove the import for `CalculatedFieldType` and `CalculatedField` from AlarmRulesTest if they are no longer used:
```java
// Remove if unused:
// import org.thingsboard.server.common.data.cf.CalculatedField;
// import org.thingsboard.server.common.data.cf.CalculatedFieldType;
```
Keep `CalculatedFieldId` import since it's still used by `getDebugEvents`.
- [ ] **Step 7: Verify test compilation**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test-compile -pl application -am -q -DskipTests 2>&1 | tail -10`
Expected: BUILD SUCCESS
- [ ] **Step 8: Run AlarmRulesTest**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe && mvn test -pl application -Dtest="org.thingsboard.server.cf.AlarmRulesTest" -DfailIfNoTests=false 2>&1 | tail -30`
Expected: All tests pass. If any test fails, investigate and fix the variable name/type mismatches.
- [ ] **Step 9: Commit**
```bash
git add application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
git add application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
git commit -m "Update AlarmRulesTest to use new alarm rule API"
```
---
### Task 5: Create UI AlarmRulesService
**Files:**
- Create: `ui-ngx/src/app/core/http/alarm-rules.service.ts`
- [ ] **Step 1: Create the service**
```typescript
///
/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL
///
/// Copyright © 2016-2026 ThingsBoard, Inc. All Rights Reserved.
///
/// NOTICE: All information contained herein is, and remains
/// the property of ThingsBoard, Inc. and its suppliers,
/// if any. The intellectual and technical concepts contained
/// herein are proprietary to ThingsBoard, Inc.
/// and its suppliers and may be covered by U.S. and Foreign Patents,
/// patents in process, and are protected by trade secret or copyright law.
///
/// Dissemination of this information or reproduction of this material is strictly forbidden
/// unless prior written permission is obtained from COMPANY.
///
/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees,
/// managers or contractors who have executed Confidentiality and Non-disclosure agreements
/// explicitly covering such access.
///
/// The copyright notice above does not evidence any actual or intended publication
/// or disclosure of this source code, which includes
/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY.
/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE,
/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT
/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED,
/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES.
/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION
/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS,
/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
///
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 {
CalculatedField,
CalculatedFieldInfo,
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<CalculatedField> {
return this.http.get<CalculatedField>(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config));
}
public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable<CalculatedField> {
return this.http.post<CalculatedField>('/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<CalculatedFieldInfo>> {
return this.http.get<PageData<CalculatedFieldInfo>>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config));
}
public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/alarm/rule/${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));
}
}
```
- [ ] **Step 2: Commit**
```bash
git add ui-ngx/src/app/core/http/alarm-rules.service.ts
git commit -m "Add AlarmRulesService for UI"
```
---
### Task 6: Update UI components to use AlarmRulesService
**Files:**
- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts`
- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts`
- Modify: `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts`
- [ ] **Step 1: Update alarm-rules-table-config.ts**
In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts`:
1. Add import for `AlarmRulesService`:
```typescript
import { AlarmRulesService } from '@core/http/alarm-rules.service';
```
2. Change the constructor parameter type from `CalculatedFieldsService` to `AlarmRulesService` (line 105):
```typescript
constructor(private alarmRulesService: AlarmRulesService,
```
3. Update all service call sites. Replace:
- `this.calculatedFieldsService.getCalculatedFieldById(id.id)``this.alarmRulesService.getAlarmRuleById(id.id)` (line 155)
- `this.calculatedFieldsService.saveCalculatedField(alarmRule)``this.alarmRulesService.saveAlarmRule(alarmRule)` (line 156)
- `this.calculatedFieldsService.deleteCalculatedField(id.id)``this.alarmRulesService.deleteAlarmRule(id.id)` (line 165)
- `this.calculatedFieldsService.saveCalculatedField(calculatedField)``this.alarmRulesService.saveAlarmRule(calculatedField)` (line 393, in `importCalculatedField`)
4. Update `fetchCalculatedFields` method (line 248-252):
```typescript
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<AlarmRuleTableEntity>> {
return this.pageMode ?
this.alarmRulesService.getAlarmRules(pageLink, this.alarmRuleFilterConfig) :
this.alarmRulesService.getAlarmRulesByEntityId(this.entityId, pageLink);
}
```
Note: The `getAlarmRules` call no longer passes `types: [CalculatedFieldType.ALARM]` since the endpoint hardcodes that. The `alarmRuleFilterConfig` query object may still contain entity type filters which will be passed as query params.
5. Update `onDebugConfigChanged` method (line 414-419):
```typescript
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
this.alarmRulesService.getAlarmRuleById(id).pipe(
switchMap(field => this.alarmRulesService.saveAlarmRule({ ...field, debugSettings })),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData());
}
```
6. Remove the `CalculatedFieldsService` import if it's no longer needed. Keep `CalculatedFieldType` import since it's still referenced in `importCalculatedField` for type validation check.
- [ ] **Step 2: Update alarm-rules-table.component.ts**
In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts`:
1. Replace `CalculatedFieldsService` import with `AlarmRulesService`:
```typescript
import { AlarmRulesService } from '@core/http/alarm-rules.service';
```
2. Change the injected service in the constructor from `CalculatedFieldsService` to `AlarmRulesService`:
```typescript
constructor(private alarmRulesService: AlarmRulesService,
```
3. Update the `AlarmRulesTableConfig` instantiation to pass `alarmRulesService` instead of `calculatedFieldsService`:
```typescript
this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.alarmRulesService,
// ... remaining parameters stay the same
);
```
- [ ] **Step 3: Update alarm-rule-dialog.component.ts**
In `ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts`:
1. Add import for `AlarmRulesService`:
```typescript
import { AlarmRulesService } from '@core/http/alarm-rules.service';
```
2. Change the constructor injection from `CalculatedFieldsService` to `AlarmRulesService`:
```typescript
private alarmRulesService: AlarmRulesService,
```
3. Update the `add()` method (around line 206) to use the new service:
```typescript
this.alarmRulesService.saveAlarmRule(alarmRule)
```
4. Remove the `CalculatedFieldsService` import if no longer used.
- [ ] **Step 4: Verify UI build**
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx ng build --configuration production 2>&1 | tail -20`
If the build is slow, alternatively verify just type checking:
Run: `cd /Users/viacheslav/Desktop/thingsboard-pe/ui-ngx && npx tsc --noEmit 2>&1 | head -30`
Expected: No compilation errors.
- [ ] **Step 5: Commit**
```bash
git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
git add ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
git commit -m "Switch UI alarm rule components to AlarmRulesService"
```

144
docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md

@ -0,0 +1,144 @@
# Alarm Rule Controller Design
## Problem
Alarm rules in ThingsBoard PE are internally implemented as calculated fields with `CalculatedFieldType.ALARM`. API consumers must create alarm rules via the `CalculatedFieldController`, setting `type = ALARM` — exposing an implementation detail that is confusing and leaks the CF abstraction.
## Goal
Create a dedicated `AlarmRuleController` that provides a clean, alarm-focused REST API while delegating to the existing `TbCalculatedFieldService` underneath. Update the UI and tests to use the new endpoints.
## Approach
Thin controller wrapper: `AlarmRuleController` delegates directly to `TbCalculatedFieldService`. A new `AlarmRuleDefinition` DTO wraps `CalculatedField` with the `type` field hidden (always ALARM). Conversion happens inline in the controller. No new service layer.
The existing `CalculatedFieldController` remains unchanged.
## DTOs
### AlarmRuleDefinition
Located in `common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java`.
Same structure as `CalculatedField` but without the `type` field:
- `id`: `CalculatedFieldId` (reused, not a new ID type)
- `tenantId`: `TenantId`
- `name`: `String`
- `entityId`: `EntityId`
- `configuration`: `AlarmCalculatedFieldConfiguration`
- `createdTime`: `long`
- `configurationVersion`: `int`
Provides conversion methods:
- `toCalculatedField()` — sets `type = ALARM`, copies all fields
- `static fromCalculatedField(CalculatedField cf)` — strips `type`, copies all fields
### AlarmRuleDefinitionInfo
Extends `AlarmRuleDefinition`, adds `entityName: String`. Mirrors the `CalculatedFieldInfo` pattern.
Provides:
- `static fromCalculatedFieldInfo(CalculatedFieldInfo cfi)` — converts from the existing info type
## Controller
### AlarmRuleController
Located in `application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java`.
- `@RequestMapping("/api/alarm/rule")` for single-entity operations
- Extends `BaseController`
- Injects `TbCalculatedFieldService`, `EventService`, `TbelInvokeService`
- All endpoints require `TENANT_ADMIN` authority
- Uses existing `Operation.WRITE_CALCULATED_FIELD` / `READ_CALCULATED_FIELD` permissions
### Endpoints
| Method | Path | Description | Delegates To |
|--------|------|-------------|--------------|
| `POST` | `/api/alarm/rule` | Create or update alarm rule | `TbCalculatedFieldService.save()` |
| `GET` | `/api/alarm/rule/{alarmRuleId}` | Get alarm rule by ID | `TbCalculatedFieldService.findById()` |
| `GET` | `/api/alarm/rule/{entityType}/{entityId}` | Get alarm rules by entity (paged) | `TbCalculatedFieldService.findByTenantIdAndEntityId()` with `type=ALARM` |
| `GET` | `/api/alarm/rules` | List all alarm rules (paged, filtered) | `calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter()` with `types={ALARM}` |
| `GET` | `/api/alarm/rules/names` | Get alarm rule names (paged) | `calculatedFieldService.findCalculatedFieldNamesByTenantIdAndType()` with `type=ALARM` |
| `DELETE` | `/api/alarm/rule/{alarmRuleId}` | Delete alarm rule | `TbCalculatedFieldService.delete()` |
| `GET` | `/api/alarm/rule/{alarmRuleId}/debug` | Get latest debug event | `EventService.findLatestEvents()` |
| `POST` | `/api/alarm/rule/testScript` | Test TBEL expression | Same logic as CF testScript |
### Save endpoint details
Accepts `AlarmRuleDefinition`. The controller:
1. Converts to `CalculatedField` via `toCalculatedField()` (sets `type = ALARM`)
2. Sets `tenantId` from the current user
3. Checks entity permissions (`Operation.WRITE_CALCULATED_FIELD`)
4. Checks referenced entities in the configuration
5. Calls `TbCalculatedFieldService.save()`
6. Converts result back to `AlarmRuleDefinition`
### List endpoint details
Accepts the same filter parameters as the CF list endpoint (entity type, entity IDs, text search, paging) but:
- Does NOT accept `types` parameter (hardcoded to `{ALARM}`)
- Does NOT accept `name` parameter (from the CF endpoint's multi-value `name` query param)
- Returns `PageData<AlarmRuleDefinitionInfo>` (converted from `PageData<CalculatedFieldInfo>`)
- Entity type filter defaults to all entity types that support ALARM (DEVICE, ASSET, CUSTOMER, DEVICE_PROFILE, ASSET_PROFILE), with the same permission check pattern as the CF controller
### Get by entity endpoint details
Accepts `entityType`, `entityId`, paging parameters. Does NOT accept `type` parameter (hardcoded to `ALARM`). Returns `PageData<AlarmRuleDefinition>`.
## UI Changes
### New service: alarm-rules.service.ts
Located in `ui-ngx/src/app/core/http/alarm-rules.service.ts`.
Follows the same `@Injectable({ providedIn: 'root' })` pattern as `CalculatedFieldsService`. Methods:
- `saveAlarmRule(rule: AlarmRuleDefinition)` -> `POST /api/alarm/rule`
- `getAlarmRuleById(id: string)` -> `GET /api/alarm/rule/{id}`
- `getAlarmRulesByEntityId(entityId: EntityId, pageLink: PageLink)` -> `GET /api/alarm/rule/{entityType}/{entityId}`
- `getAlarmRules(pageLink: PageLink, query)` -> `GET /api/alarm/rules`
- `getAlarmRuleNames(pageLink: PageLink)` -> `GET /api/alarm/rules/names`
- `deleteAlarmRule(id: string)` -> `DELETE /api/alarm/rule/{id}`
- `getLatestAlarmRuleDebugEvent(id: string)` -> `GET /api/alarm/rule/{id}/debug`
- `testScript(inputParams)` -> `POST /api/alarm/rule/testScript`
### Consumer updates
All components that currently call `CalculatedFieldsService` for ALARM-type operations switch to `AlarmRulesService`. Primary consumers:
- `AlarmRuleDialogComponent` — switches `saveCalculatedField()` to `saveAlarmRule()`
- Any component loading alarm rules by entity — switches to `getAlarmRulesByEntityId()`
- Components listing alarm rules — switches to `getAlarmRules()`
The TypeScript model type `CalculatedFieldAlarmRule` can be updated or aliased to match the new `AlarmRuleDefinition` shape (without the `type` discriminator field in the request).
## Test Changes
### AlarmRulesTest.java
Located at `application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java`.
Updates:
- `saveCalculatedField()` helper changes from `POST /api/calculatedField` to `POST /api/alarm/rule`
- The helper builds `AlarmRuleDefinition` instead of `CalculatedField` (no `type` field, alarm configuration directly on the object)
- GET calls for alarm rules by entity change to `/api/alarm/rule/{entityType}/{entityId}`
- Debug event retrieval may optionally use `/api/alarm/rule/{id}/debug` instead of direct `EventDao` access (though the test currently uses `EventDao` directly for most checks, which is fine)
- The `createAlarmCf()` helper is renamed to `createAlarmRule()` and returns `AlarmRuleDefinition`
## Scope boundaries
**In scope:**
- New `AlarmRuleDefinition` and `AlarmRuleDefinitionInfo` DTOs
- New `AlarmRuleController` with all listed endpoints
- New `AlarmRulesService` on the UI side
- Update `AlarmRulesTest` to use new API
- Update UI components to use new service
**Out of scope:**
- Changes to `CalculatedFieldController` (left as-is)
- New permission types (reuses `READ_CALCULATED_FIELD` / `WRITE_CALCULATED_FIELD`)
- Reprocessing endpoints (not applicable to alarm rules)
- New `TbAlarmRuleService` layer (thin wrapper approach, no new service)

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 {
CalculatedField,
CalculatedFieldInfo,
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<CalculatedField> {
return this.http.get<CalculatedField>(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config));
}
public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable<CalculatedField> {
return this.http.post<CalculatedField>('/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<CalculatedFieldInfo>> {
return this.http.get<PageData<CalculatedFieldInfo>>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config));
}
public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/alarm/rule/${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));
}
}

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

@ -25,7 +25,7 @@ import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@
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";
@ -97,7 +97,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private alarmRulesService: AlarmRulesService,
private destroyRef: DestroyRef,
private cfFormService: CalculatedFieldFormService) {
super(store, router, dialogRef);
@ -173,7 +173,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),

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

@ -35,7 +35,7 @@ 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,
@ -84,7 +84,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 +128,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);
@ -216,8 +216,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 {
@ -353,7 +353,7 @@ 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)
)
@ -375,8 +375,8 @@ 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());

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,

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,

Loading…
Cancel
Save