From 5768bbefe16ddcc69f1892ad45f87de3f7faaa75 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 31 Mar 2026 11:12:14 +0300 Subject: [PATCH] Add separate alarm rules API --- .../controller/AlarmRuleController.java | 365 +++++++ .../thingsboard/server/cf/AlarmRulesTest.java | 178 ++-- .../server/controller/AbstractWebTest.java | 5 + .../common/data/cf/AlarmRuleDefinition.java | 165 +++ .../data/cf/AlarmRuleDefinitionInfo.java | 39 + .../plans/2026-03-30-alarm-rule-controller.md | 985 ++++++++++++++++++ ...2026-03-30-alarm-rule-controller-design.md | 144 +++ .../src/app/core/http/alarm-rules.service.ts | 73 ++ .../alarm-rule-dialog.component.ts | 6 +- .../alarm-rules/alarm-rules-table-config.ts | 20 +- .../alarm-rules-table.component.ts | 6 +- .../home/pages/alarm/alarm-routing.module.ts | 6 +- 12 files changed, 1883 insertions(+), 109 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java create mode 100644 docs/superpowers/plans/2026-03-30-alarm-rule-controller.md create mode 100644 docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md create mode 100644 ui-ngx/src/app/core/http/alarm-rules.service.ts diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmRuleController.java new file mode 100644 index 0000000000..dd3b51fc5d --- /dev/null +++ b/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 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 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 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 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 types = EnumSet.of(CalculatedFieldType.ALARM); + + Set 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 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 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 arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList 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 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 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."); + } + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index cf94940e07..7041a71086 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/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(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(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 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 assertion) { - return checkAlarmResult(calculatedField, null, assertion); + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion) { + return checkAlarmResult(alarmRule, null, assertion); } - private TbAlarmResult checkAlarmResult(CalculatedField calculatedField, + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Predicate waitFor, Consumer 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 arguments, - Map createConditions, - Condition clearCondition, - Consumer... modifier) { + private AlarmRuleDefinition createAlarmRule(EntityId entityId, + String alarmType, + Map arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { Map 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) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 6a658ffd66..055e1e19a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/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 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); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinition.java new file mode 100644 index 0000000000..67b743b3bb --- /dev/null +++ b/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 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(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/AlarmRuleDefinitionInfo.java new file mode 100644 index 0000000000..145dfc4081 --- /dev/null +++ b/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()); + } + +} diff --git a/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md b/docs/superpowers/plans/2026-03-30-alarm-rule-controller.md new file mode 100644 index 0000000000..af113ceaf1 --- /dev/null +++ b/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 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 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 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 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 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 types = EnumSet.of(CalculatedFieldType.ALARM); + + Set 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 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 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 arguments = Objects.requireNonNullElse( + JacksonUtil.convertValue(inputParams.get("arguments"), new TypeReference<>() {}), + Collections.emptyMap() + ); + + ArrayList 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 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 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 arguments, + Map createConditions, + Condition clearCondition, + Consumer... modifier) { + Map 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 assertion) { + return checkAlarmResult(alarmRule, assertion, null); + } + + private TbAlarmResult checkAlarmResult(AlarmRuleDefinition alarmRule, Consumer assertion, Predicate 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 { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/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> { + 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" +``` diff --git a/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md b/docs/superpowers/specs/2026-03-30-alarm-rule-controller-design.md new file mode 100644 index 0000000000..3c99e84fb5 --- /dev/null +++ b/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` (converted from `PageData`) +- 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`. + +## 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) diff --git a/ui-ngx/src/app/core/http/alarm-rules.service.ts b/ui-ngx/src/app/core/http/alarm-rules.service.ts new file mode 100644 index 0000000000..1800b2a475 --- /dev/null +++ b/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 { + return this.http.get(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAlarmRule(alarmRule: CalculatedField, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule', alarmRule, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmRule(alarmRuleId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/rule/${alarmRuleId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRules(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + + public getAlarmRulesByEntityId({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rule/${entityType}/${id}${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { + return this.http.post('/api/alarm/rule/testScript', inputParams, defaultHttpOptionsFromConfig(config)); + } + + public getLatestAlarmRuleDebugEvent(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/alarm/rule/${id}/debug`, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmRuleNames(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/rules/names${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index 62cee00513..97cdd91689 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/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, - 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 this.dialogRef.close(calculatedField), diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 039ee7d5fd..06b9213e02 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/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 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> { 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 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 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()); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index a13aa2fac7..ce0aecb598 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/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, @@ -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, diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts index b0606b9c5b..75dd645b40 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts +++ b/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 = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot, - calculatedFieldsService = inject(CalculatedFieldsService), + alarmRulesService = inject(AlarmRulesService), translate = inject(TranslateService), dialog = inject(MatDialog), store = inject(Store), @@ -52,7 +52,7 @@ export const AlarmRulesTableConfigResolver: ResolveFn = router = inject(Router), ) => { return new AlarmRulesTableConfig( - calculatedFieldsService, + alarmRulesService, translate, dialog, datePipe,