Browse Source

Merge remote-tracking branch 'klimov/feature/notification-system' into feature/notification-system

pull/8046/head
Vladyslav_Prykhodko 4 years ago
parent
commit
91440c41f9
  1. 4
      application/src/main/data/upgrade/3.4.3/schema_update.sql
  2. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
  4. 2
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  5. 43
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java
  6. 13
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
  7. 4
      application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
  8. 10
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  9. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  10. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
  11. 18
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  12. 28
      application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java
  13. 39
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java
  14. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/NotificationRuleProcessingService.java
  15. 52
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  16. 24
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  17. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java
  18. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  19. 110
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java
  20. 10
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  21. 92
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  22. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  23. 38
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java
  24. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  25. 45
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  26. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java
  27. 56
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java
  28. 28
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java
  29. 4
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java
  30. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java
  31. 41
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
  32. 4
      dao/src/main/resources/sql/schema-entities.sql
  33. 23
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html
  34. 8
      ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts
  35. 17
      ui-ngx/src/app/shared/models/notification.models.ts
  36. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/src/main/data/upgrade/3.4.3/schema_update.sql

@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS notification_template (
created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
notification_type VARCHAR(32) NOT NULL,
notification_type VARCHAR(50) NOT NULL,
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name)
);
@ -84,7 +84,7 @@ CREATE TABLE IF NOT EXISTS notification (
created_time BIGINT NOT NULL,
request_id UUID NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
type VARCHAR(32) NOT NULL,
type VARCHAR(50) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
additional_config VARCHAR(1000),

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

@ -95,6 +95,7 @@ import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.notification.rule.NotificationRuleProcessingService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
@ -334,6 +335,10 @@ public class ActorSystemContext {
@Getter
private NotificationCenter notificationCenter;
@Autowired
@Getter
private NotificationRuleProcessingService notificationRuleProcessingService;
@Autowired
@Getter
private SlackService slackService;

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java

@ -20,7 +20,6 @@ import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
@ -30,7 +29,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
public class RuleChainActor extends RuleEngineComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
private final RuleChain ruleChain;
@ -101,6 +100,16 @@ public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMe
}
}
@Override
protected RuleChainId getRuleChainId() {
return ruleChain.getId();
}
@Override
protected String getRuleChainName() {
return ruleChain.getName();
}
@Override
protected long getErrorPersistFrequency() {
return systemContext.getRuleChainErrorPersistFrequency();

2
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java

@ -94,7 +94,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
@Override
public String getComponentName() {
return null;
return ruleChainName;
}
@Override

43
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleEngineComponentActor.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2022 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.actors.ruleChain;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
public abstract class RuleEngineComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ComponentActor<T, P> {
public RuleEngineComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) {
super(systemContext, tenantId, id);
}
@Override
protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
super.logLifecycleEvent(event, e);
systemContext.getNotificationRuleProcessingService().process(tenantId, getRuleChainId(), getRuleChainName(),
id, processor.getComponentName(), event, e);
}
protected abstract RuleChainId getRuleChainId();
protected abstract String getRuleChainName();
}

13
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java

@ -21,7 +21,6 @@ import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorId;
import org.thingsboard.server.actors.TbEntityActorId;
import org.thingsboard.server.actors.service.ComponentActor;
import org.thingsboard.server.actors.service.ContextBasedCreator;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -32,7 +31,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
@Slf4j
public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
public class RuleNodeActor extends RuleEngineComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
private final String ruleChainName;
private final RuleChainId ruleChainId;
@ -133,6 +132,16 @@ public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessa
}
}
@Override
protected RuleChainId getRuleChainId() {
return ruleChainId;
}
@Override
protected String getRuleChainName() {
return ruleChainName;
}
@Override
protected long getErrorPersistFrequency() {
return systemContext.getRuleNodeErrorPersistFrequency();

4
application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java

@ -17,7 +17,6 @@ package org.thingsboard.server.actors.service;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActor;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorException;
import org.thingsboard.server.actors.TbRuleNodeUpdateException;
@ -181,9 +180,10 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
logLifecycleEvent(event, null);
}
private void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
systemContext.persistLifecycleEvent(tenantId, id, event, e);
}
protected abstract long getErrorPersistFrequency();
}

10
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.CustomerId;
@ -89,6 +90,12 @@ public class EntityActionService {
case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE;
break;
case ADDED_COMMENT:
msgType = DataConstants.COMMENT_CREATED;
break;
case UPDATED_COMMENT:
msgType = DataConstants.COMMENT_UPDATED;
break;
case ASSIGNED_FROM_TENANT:
msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT;
break;
@ -163,6 +170,9 @@ public class EntityActionService {
String strEdgeName = extractParameter(String.class, 2, additionalInfo);
metaData.putValue("unassignedEdgeId", strEdgeId);
metaData.putValue("unassignedEdgeName", strEdgeName);
} else if (actionType == ActionType.ADDED_COMMENT || actionType == ActionType.UPDATED_COMMENT) {
AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo);
metaData.putValue("comment", json.writeValueAsString(comment));
}
ObjectNode entityNode;
if (entity != null) {

6
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -34,7 +35,6 @@ 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.TimePageLink;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.edge.EdgeService;
@ -63,11 +63,9 @@ public abstract class AbstractTbEntityService {
protected EdgeService edgeService;
@Autowired
protected AlarmService alarmService;
@Autowired
@Autowired @Lazy
protected AlarmSubscriptionService alarmSubscriptionService;
@Autowired
protected AlarmCommentService alarmCommentService;
@Autowired
protected CustomerService customerService;
@Autowired
protected TbClusterService tbClusterService;

6
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.entitiy.alarm;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -24,11 +25,16 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.alarm.AlarmCommentService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service
@AllArgsConstructor
public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{
@Autowired
private AlarmCommentService alarmCommentService;
@Override
public AlarmComment saveAlarmComment(Alarm alarm, AlarmComment alarmComment, User user) throws ThingsboardException {
ActionType actionType = alarmComment.getId() == null ? ActionType.ADDED_COMMENT : ActionType.UPDATED_COMMENT;

18
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -19,6 +19,8 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
@ -37,8 +39,12 @@ import java.util.List;
@Service
@AllArgsConstructor
@Slf4j
public class DefaultTbAlarmService extends AbstractTbEntityService implements TbAlarmService {
@Autowired
protected TbAlarmCommentService alarmCommentService;
@Override
public Alarm save(Alarm alarm, User user) throws ThingsboardException {
ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
@ -65,7 +71,11 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString()))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
alarm.setAckTs(ackTs);
alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK);
notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user);
@ -85,7 +95,11 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString()))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
alarm.setClearTs(clearTs);
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user);

28
application/src/main/java/org/thingsboard/server/service/notification/DefaultNotificationCenter.java

@ -42,7 +42,6 @@ import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -118,8 +117,10 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
throw new IllegalArgumentException("Slack must be configured in the settings");
}
}
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing");
if (notificationRequest.getRuleId() == null) {
if (targets.stream().noneMatch(target -> target.getConfiguration().getType().getSupportedDeliveryMethods().contains(deliveryMethod))) {
throw new IllegalArgumentException("Target for " + deliveryMethod.getName() + " delivery method is missing");
}
}
});
@ -204,15 +205,17 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
log.debug("[{}] Processing notification request for {} target ({}) for delivery methods {}", ctx.getRequest().getId(), target.getConfiguration().getType(), target.getId(), deliveryMethods);
List<ListenableFuture<Void>> results = new ArrayList<>();
for (NotificationRecipient recipient : recipients) {
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
ListenableFuture<Void> resultFuture = processForRecipient(deliveryMethod, recipient, ctx);
DonAsynchron.withCallback(resultFuture, result -> {
ctx.getStats().reportSent(deliveryMethod, recipient);
}, error -> {
ctx.getStats().reportError(deliveryMethod, error, recipient);
});
results.add(resultFuture);
if (!deliveryMethods.isEmpty()) {
for (NotificationRecipient recipient : recipients) {
for (NotificationDeliveryMethod deliveryMethod : deliveryMethods) {
ListenableFuture<Void> resultFuture = processForRecipient(deliveryMethod, recipient, ctx);
DonAsynchron.withCallback(resultFuture, result -> {
ctx.getStats().reportSent(deliveryMethod, recipient);
}, error -> {
ctx.getStats().reportError(deliveryMethod, error, recipient);
});
results.add(resultFuture);
}
}
}
return results;
@ -333,6 +336,7 @@ public class DefaultNotificationCenter extends AbstractSubscriptionService imple
// marking related notifications as unread: FIXME: causes each subscription to fetch notifications on each request update
notificationService.updateNotificationsStatusByRequestId(tenantId, notificationRequest.getId(), NotificationStatus.SENT);
// TODO: no need to update request with other than PLATFORM_USERS target type
onNotificationRequestUpdate(tenantId, NotificationRequestUpdate.builder()
.notificationRequestId(notificationRequest.getId())
.notificationInfo(notificationRequest.getInfo())

39
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessingService.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestConfig;
@ -45,7 +46,9 @@ import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.NotificationExecutorService;
import org.thingsboard.server.service.notification.rule.trigger.AlarmTriggerProcessor.AlarmTriggerObject;
import org.thingsboard.server.service.notification.rule.trigger.NotificationRuleTriggerProcessor;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineComponentLifecycleEventTriggerProcessor.RuleEngineComponentLifecycleEventTriggerObject;
import java.util.Collection;
import java.util.List;
@ -71,7 +74,9 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
DataConstants.INACTIVITY_EVENT, NotificationRuleTriggerType.DEVICE_INACTIVITY,
DataConstants.ENTITY_CREATED, NotificationRuleTriggerType.ENTITY_ACTION,
DataConstants.ENTITY_UPDATED, NotificationRuleTriggerType.ENTITY_ACTION,
DataConstants.ENTITY_DELETED, NotificationRuleTriggerType.ENTITY_ACTION
DataConstants.ENTITY_DELETED, NotificationRuleTriggerType.ENTITY_ACTION,
DataConstants.COMMENT_CREATED, NotificationRuleTriggerType.ALARM_COMMENT,
DataConstants.COMMENT_UPDATED, NotificationRuleTriggerType.ALARM_COMMENT
);
@Override
@ -82,37 +87,52 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
return;
}
processTrigger(tenantId, triggerType, ruleEngineMsg.getOriginator(), ruleEngineMsg, false);
processTrigger(tenantId, triggerType, ruleEngineMsg.getOriginator(), ruleEngineMsg);
}
@Override
public void process(TenantId tenantId, Alarm alarm, boolean deleted) {
processTrigger(tenantId, NotificationRuleTriggerType.ALARM, alarm.getId(), alarm, deleted);
AlarmTriggerObject triggerObject = AlarmTriggerObject.builder()
.alarm(alarm)
.deleted(deleted)
.build();
processTrigger(tenantId, NotificationRuleTriggerType.ALARM, alarm.getId(), triggerObject);
}
@Override
public void process(TenantId tenantId, RuleChainId ruleChainId, String ruleChainName, EntityId componentId, String componentName, ComponentLifecycleEvent eventType, Exception error) {
RuleEngineComponentLifecycleEventTriggerObject triggerObject = RuleEngineComponentLifecycleEventTriggerObject.builder()
.ruleChainId(ruleChainId)
.ruleChainName(ruleChainName)
.componentId(componentId)
.componentName(componentName)
.eventType(eventType)
.error(error)
.build();
processTrigger(tenantId, NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, componentId, triggerObject);
}
private void processTrigger(TenantId tenantId, NotificationRuleTriggerType triggerType, EntityId originatorEntityId,
Object triggerObject, boolean triggerRemoved) {
private void processTrigger(TenantId tenantId, NotificationRuleTriggerType triggerType, EntityId originatorEntityId, Object triggerObject) {
ListenableFuture<List<NotificationRule>> rulesFuture = dbCallbackExecutor.submit(() -> {
return notificationRuleService.findNotificationRulesByTenantIdAndTriggerType(tenantId, triggerType);
});
DonAsynchron.withCallback(rulesFuture, rules -> {
for (NotificationRule rule : rules) {
notificationExecutor.submit(() -> {
processNotificationRule(rule, originatorEntityId, triggerObject, triggerRemoved);
processNotificationRule(rule, originatorEntityId, triggerObject);
});
}
}, e -> {});
}
private <T> void processNotificationRule(NotificationRule rule, EntityId originatorEntityId,
T triggerObject, boolean triggerRemoved) {
private void processNotificationRule(NotificationRule rule, EntityId originatorEntityId, Object triggerObject) {
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig();
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType());
if (triggerConfig.getTriggerType().isUpdatable()) {
List<NotificationRequest> notificationRequests = notificationRequestService.findNotificationRequestsByRuleIdAndOriginatorEntityId(rule.getTenantId(), rule.getId(), originatorEntityId);
if (!notificationRequests.isEmpty()) {
if (triggerRemoved || matchesClearRule(triggerObject, triggerConfig)) {
if (matchesClearRule(triggerObject, triggerConfig)) {
notificationRequests = notificationRequests.stream()
.filter(notificationRequest -> {
if (!notificationRequest.isSent()) {
@ -133,7 +153,6 @@ public class DefaultNotificationRuleProcessingService implements NotificationRul
NotificationInfo previousNotificationInfo = notificationRequest.getInfo();
if (!notificationInfo.equals(previousNotificationInfo)) {
notificationRequest.setInfo(notificationInfo);
// and make notifications unread ?
dbCallbackExecutor.submit(() -> {
notificationCenter.updateNotificationRequest(rule.getTenantId(), notificationRequest);
});

6
application/src/main/java/org/thingsboard/server/service/notification/rule/NotificationRuleProcessingService.java

@ -16,7 +16,10 @@
package org.thingsboard.server.service.notification.rule;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg;
public interface NotificationRuleProcessingService {
@ -25,4 +28,7 @@ public interface NotificationRuleProcessingService {
void process(TenantId tenantId, Alarm alarm, boolean deleted);
void process(TenantId tenantId, RuleChainId ruleChainId, String ruleChainName,
EntityId componentId, String componentName, ComponentLifecycleEvent eventType, Exception error);
}

52
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.notification.info.AlarmCommentNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg;
@Service
public class AlarmCommentTriggerProcessor implements NotificationRuleTriggerProcessor<TbMsg, AlarmCommentNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(TbMsg ruleEngineMsg, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
return ruleEngineMsg.getMetaData().getValue("comment") != null;
}
@Override
public NotificationInfo constructNotificationInfo(TbMsg ruleEngineMsg, AlarmCommentNotificationRuleTriggerConfig triggerConfig) {
AlarmComment comment = JacksonUtil.fromString(ruleEngineMsg.getMetaData().getValue("comment"), AlarmComment.class);
Alarm alarm = JacksonUtil.fromString(ruleEngineMsg.getData(), Alarm.class);
return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText())
.alarmType(alarm.getType())
.alarmId(comment.getAlarmId().getId())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_COMMENT;
}
}

24
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmNotificationRuleTriggerProcessor.java → application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -23,18 +25,24 @@ import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.service.notification.rule.trigger.AlarmTriggerProcessor.AlarmTriggerObject;
@Service
public class AlarmNotificationRuleTriggerProcessor implements NotificationRuleTriggerProcessor<Alarm, AlarmNotificationRuleTriggerConfig> {
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTriggerObject, AlarmNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
public boolean matchesFilter(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) {
Alarm alarm = triggerObject.getAlarm();
return (CollectionUtils.isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) &&
(CollectionUtils.isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity()));
}
@Override
public boolean matchesClearRule(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
public boolean matchesClearRule(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) {
if (triggerObject.isDeleted()) {
return true;
}
Alarm alarm = triggerObject.getAlarm();
ClearRule clearRule = triggerConfig.getClearRule();
if (clearRule != null) {
if (clearRule.getAlarmStatus() != null) {
@ -45,7 +53,8 @@ public class AlarmNotificationRuleTriggerProcessor implements NotificationRuleTr
}
@Override
public NotificationInfo constructNotificationInfo(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
public NotificationInfo constructNotificationInfo(AlarmTriggerObject triggerObject, AlarmNotificationRuleTriggerConfig triggerConfig) {
Alarm alarm = triggerObject.getAlarm();
return AlarmNotificationInfo.builder()
.alarmId(alarm.getUuidId())
.alarmType(alarm.getType())
@ -61,4 +70,11 @@ public class AlarmNotificationRuleTriggerProcessor implements NotificationRuleTr
return NotificationRuleTriggerType.ALARM;
}
@Data
@Builder
public static class AlarmTriggerObject {
private final Alarm alarm;
private final boolean deleted;
}
}

2
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityNotificationRuleTriggerProcessor.java → application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/DeviceInactivityTriggerProcessor.java

@ -30,7 +30,7 @@ import org.thingsboard.server.service.profile.TbDeviceProfileCache;
@Service
@RequiredArgsConstructor
public class DeviceInactivityNotificationRuleTriggerProcessor implements NotificationRuleTriggerProcessor<TbMsg, DeviceInactivityNotificationRuleTriggerConfig> {
public class DeviceInactivityTriggerProcessor implements NotificationRuleTriggerProcessor<TbMsg, DeviceInactivityNotificationRuleTriggerConfig> {
private final TbDeviceProfileCache deviceProfileCache;

2
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionNotificationRuleTriggerProcessor.java → application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.UUID;
@Service
public class EntityActionNotificationRuleTriggerProcessor implements NotificationRuleTriggerProcessor<TbMsg, EntityActionNotificationRuleTriggerConfig> {
public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor<TbMsg, EntityActionNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(TbMsg ruleEngineMsg, EntityActionNotificationRuleTriggerConfig triggerConfig) {

110
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/RuleEngineComponentLifecycleEventTriggerProcessor.java

@ -0,0 +1,110 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.notification.rule.trigger;
import com.google.common.base.Strings;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleEngineComponentLifecycleEventNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.service.notification.rule.trigger.RuleEngineComponentLifecycleEventTriggerProcessor.RuleEngineComponentLifecycleEventTriggerObject;
import java.util.Set;
@Service
public class RuleEngineComponentLifecycleEventTriggerProcessor implements NotificationRuleTriggerProcessor<RuleEngineComponentLifecycleEventTriggerObject, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(RuleEngineComponentLifecycleEventTriggerObject triggerObject, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) {
if (CollectionUtils.isNotEmpty(triggerConfig.getRuleChains())) {
if (!triggerConfig.getRuleChains().contains(triggerObject.getRuleChainId().getId())) {
return false;
}
}
EntityType componentType = triggerObject.getComponentId().getEntityType();
Set<ComponentLifecycleEvent> trackedEvents;
boolean onlyFailures;
if (componentType == EntityType.RULE_CHAIN) {
trackedEvents = triggerConfig.getRuleChainEvents();
onlyFailures = triggerConfig.isOnlyRuleChainLifecycleFailures();
} else if (componentType == EntityType.RULE_NODE && triggerConfig.isTrackRuleNodeEvents()) {
trackedEvents = triggerConfig.getRuleNodeEvents();
onlyFailures = triggerConfig.isOnlyRuleNodeLifecycleFailures();
} else {
return false;
}
if (CollectionUtils.isEmpty(trackedEvents)) {
trackedEvents = Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED);
}
if (!trackedEvents.contains(triggerObject.getEventType())) {
return false;
}
if (onlyFailures) {
return triggerObject.getError() != null;
}
return true;
}
@Override
public NotificationInfo constructNotificationInfo(RuleEngineComponentLifecycleEventTriggerObject triggerObject, RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig) {
return RuleEngineComponentLifecycleEventNotificationInfo.builder()
.ruleChainId(triggerObject.getRuleChainId())
.ruleChainName(triggerObject.getRuleChainName())
.componentId(triggerObject.getComponentId())
.componentName(triggerObject.getComponentName())
.eventType(triggerObject.getEventType())
.error(getErrorMsg(triggerObject.getError()))
.build();
}
private String getErrorMsg(Exception error) {
String errorMsg = error != null ? error.getMessage() : null;
errorMsg = Strings.nullToEmpty(errorMsg);
int lengthLimit = 150;
if (errorMsg.length() > lengthLimit) {
errorMsg = StringUtils.substring(errorMsg, 0, lengthLimit + 1).trim() + "[...]";
}
return errorMsg;
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT;
}
@Data
@Builder
public static class RuleEngineComponentLifecycleEventTriggerObject {
private final RuleChainId ruleChainId;
private final String ruleChainName;
private final EntityId componentId;
private final String componentName;
private final ComponentLifecycleEvent eventType;
private final Exception error;
}
}

10
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -49,6 +50,7 @@ import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -64,7 +66,7 @@ import java.util.Optional;
public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService implements AlarmSubscriptionService {
private final AlarmService alarmService;
private final AlarmCommentService alarmCommentService;
private final TbAlarmCommentService alarmCommentService;
private final TbApiUsageReportClient apiUsageClient;
private final TbApiUsageStateService apiUsageStateService;
@ -85,7 +87,11 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity())))
.build();
alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment);
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, null);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
}
}
if (result.isCreated()) {

92
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -24,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -40,6 +42,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.NotificationRuleId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.NotificationRequest;
@ -53,13 +56,19 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.notification.NotificationRequestService;
@ -130,7 +139,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
getWsClient().waitForUpdate(true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("ADDED: DEVICE [" + device.getId() + "]");
assertThat(notification.getSubject()).isEqualTo("added: DEVICE [" + device.getId() + "]");
assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL);
@ -140,7 +149,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("UPDATED: DEVICE [" + device.getId() + "]");
assertThat(notification.getSubject()).isEqualTo("updated: DEVICE [" + device.getId() + "]");
getWsClient().registerWaitForUpdate();
@ -148,7 +157,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("DELETED: DEVICE [" + device.getId() + "]");
assertThat(notification.getSubject()).isEqualTo("deleted: DEVICE [" + device.getId() + "]");
}
@Test
@ -307,6 +316,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
}
@Test
public void testNotificationRuleProcessing_ruleEngineComponentLifecycleEvent_ruleNodeStartError() {
String subject = "Rule Node '${componentName}' in Rule Chain '${ruleChainName}' failed to start";
String text = "The error: ${error}";
NotificationTemplate template = createNotificationTemplate(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, subject, text, NotificationDeliveryMethod.PUSH);
NotificationRule rule = new NotificationRule();
rule.setName("Rule node start-up failures in my rule chain");
rule.setTemplateId(template.getId());
rule.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT);
RuleChain ruleChain = createEmptyRuleChain("My Rule Chain");
var triggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig();
triggerConfig.setRuleChains(Set.of(ruleChain.getUuidId()));
triggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED));
triggerConfig.setOnlyRuleChainLifecycleFailures(true);
triggerConfig.setTrackRuleNodeEvents(true);
triggerConfig.setRuleNodeEvents(Set.of(ComponentLifecycleEvent.STARTED));
triggerConfig.setOnlyRuleNodeLifecycleFailures(true);
rule.setTriggerConfig(triggerConfig);
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT);
recipientsConfig.setTargets(List.of(target.getUuidId()));
rule.setRecipientsConfig(recipientsConfig);
rule = saveNotificationRule(rule);
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
getWsClient().registerWaitForUpdate();
addRuleNodeWithError(ruleChain.getId(), "My generator");
getWsClient().waitForUpdate(10000, true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getType()).isEqualTo(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT);
assertThat(notification.getSubject()).isEqualTo("Rule Node 'My generator' in Rule Chain 'My Rule Chain' failed to start");
assertThat(notification.getText()).startsWith("The error: Can't compile script");
}
@Test
public void testNotificationRuleInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.PUSH, NotificationDeliveryMethod.EMAIL};
@ -368,6 +419,41 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
return deviceProfile;
}
private RuleChain createEmptyRuleChain(String name) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName(name);
ruleChain.setTenantId(tenantId);
ruleChain.setRoot(false);
ruleChain.setDebugMode(false);
ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
metaData.setNodes(List.of());
metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
return ruleChain;
}
private RuleNode addRuleNodeWithError(RuleChainId ruleChainId, String name) {
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChainId);
RuleNode generatorNodeWithError = new RuleNode();
generatorNodeWithError.setName(name);
generatorNodeWithError.setType(TbMsgGeneratorNode.class.getName());
TbMsgGeneratorNodeConfiguration generatorNodeConfiguration = new TbMsgGeneratorNodeConfiguration();
generatorNodeConfiguration.setScriptLang(ScriptLanguage.JS);
generatorNodeConfiguration.setPeriodInSeconds(1000);
generatorNodeConfiguration.setMsgCount(1);
generatorNodeConfiguration.setJsScript("[return");
generatorNodeWithError.setConfiguration(mapper.valueToTree(generatorNodeConfiguration));
metaData.setNodes(List.of(generatorNodeWithError));
metaData.setFirstNodeIndex(0);
metaData = doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class);
return metaData.getNodes().get(0);
}
private NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}

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

@ -75,6 +75,8 @@ public class DataConstants {
public static final String ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ALARM_DELETE = "ALARM_DELETE";
public static final String COMMENT_CREATED = "COMMENT_CREATED";
public static final String COMMENT_UPDATED = "COMMENT_UPDATED";
public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT";
public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT";
public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS";

38
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationProcessingContext.java

@ -15,6 +15,9 @@
*/
package org.thingsboard.server.common.data.notification;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.google.common.base.Strings;
import lombok.Builder;
import lombok.Getter;
@ -22,7 +25,6 @@ import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationDeliveryMethodConfig;
@ -31,10 +33,13 @@ import org.thingsboard.server.common.data.notification.template.DeliveryMethodNo
import org.thingsboard.server.common.data.notification.template.HasSubject;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.PushDeliveryMethodNotificationTemplate;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@SuppressWarnings("unchecked")
@ -89,10 +94,11 @@ public class NotificationProcessingContext {
return (C) settings.getDeliveryMethodsConfigs().get(deliveryMethod);
}
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map<String, String> templateContext) {
if (request.getInfo() != null && deliveryMethod != NotificationDeliveryMethod.PUSH) { // for push notifications we are processing template from info on each serialization
public <T extends DeliveryMethodNotificationTemplate> T getProcessedTemplate(NotificationDeliveryMethod deliveryMethod, Map<String, String> templateContext) {
NotificationInfo info = request.getInfo();
if (info != null && deliveryMethod != NotificationDeliveryMethod.PUSH) { // for push notifications we are processing template from info on each serialization
templateContext = new HashMap<>(templateContext);
templateContext.putAll(request.getInfo().getTemplateData());
templateContext.putAll(info.getTemplateData());
}
T template = (T) templates.get(deliveryMethod).copy();
@ -101,14 +107,30 @@ public class NotificationProcessingContext {
String subject = ((HasSubject) template).getSubject();
((HasSubject) template).setSubject(processTemplate(subject, templateContext));
}
if (deliveryMethod == NotificationDeliveryMethod.PUSH) {
PushDeliveryMethodNotificationTemplate pushNotificationTemplate = (PushDeliveryMethodNotificationTemplate) template;
Optional<ObjectNode> buttonConfig = Optional.ofNullable(pushNotificationTemplate.getAdditionalConfig())
.map(config -> config.get("actionButtonConfig")).filter(JsonNode::isObject)
.map(config -> (ObjectNode) config);
if (buttonConfig.isPresent()) {
JsonNode link = buttonConfig.get().get("link");
if (link != null && link.isTextual()) {
link = new TextNode(processTemplate(link.asText(), templateContext, info != null ? info.getTemplateData() : Collections.emptyMap()));
buttonConfig.get().set("link", link);
}
}
}
return template;
}
private static String processTemplate(String template, Map<String, String> context) {
if (template == null || context.isEmpty()) return template;
private static String processTemplate(String template, Map<String, String>... contexts) {
if (template == null) return null;
String result = template;
for (Map.Entry<String, String> kv : context.entrySet()) {
result = result.replace("${" + kv.getKey() + '}', kv.getValue());
for (Map<String, String> context : contexts) {
for (Map.Entry<String, String> kv : context.entrySet()) {
result = result.replace("${" + kv.getKey() + '}', kv.getValue());
}
}
return result;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -20,6 +20,8 @@ public enum NotificationType {
GENERAL,
ALARM,
DEVICE_INACTIVITY,
ENTITY_ACTION;
ENTITY_ACTION,
ALARM_COMMENT,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT
}

45
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2022 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.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
import java.util.UUID;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmCommentNotificationInfo implements NotificationInfo {
private String comment;
private String alarmType;
private UUID alarmId;
@Override
public Map<String, String> getTemplateData() {
return Map.of(
"comment", comment,
"alarmType", alarmType,
"alarmId", alarmId.toString()
);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java

@ -51,7 +51,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
"entityType", entityType.name(),
"entityId", entityId.toString(),
"entityName", entityName,
"actionType", actionType.name(),
"actionType", actionType.name().toLowerCase(),
"originatorUserId", originatorUserId.toString(),
"originatorUserName", originatorUserName
);

56
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/RuleEngineComponentLifecycleEventNotificationInfo.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2022 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.notification.info;
import com.google.common.base.Strings;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RuleEngineComponentLifecycleEventNotificationInfo implements NotificationInfo {
private RuleChainId ruleChainId;
private String ruleChainName;
private EntityId componentId;
private String componentName;
private ComponentLifecycleEvent eventType;
private String error;
@Override
public Map<String, String> getTemplateData() {
return Map.of(
"ruleChainId", ruleChainId.toString(),
"ruleChainName", ruleChainName,
"componentId", componentId.toString(),
"componentType", componentId.getEntityType().name(),
"componentName", componentName,
"eventType", eventType.name(),
"error", error
);
}
}

28
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 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.notification.rule.trigger;
import lombok.Data;
@Data
public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ALARM_COMMENT;
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerConfig.java

@ -25,7 +25,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes({
@Type(value = AlarmNotificationRuleTriggerConfig.class, name = "ALARM"),
@Type(value = DeviceInactivityNotificationRuleTriggerConfig.class, name = "DEVICE_INACTIVITY"),
@Type(value = EntityActionNotificationRuleTriggerConfig.class, name = "ENTITY_ACTION")
@Type(value = EntityActionNotificationRuleTriggerConfig.class, name = "ENTITY_ACTION"),
@Type(value = AlarmCommentNotificationRuleTriggerConfig.class, name = "ALARM_COMMENT"),
@Type(value = RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.class, name = "RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT")
})
public interface NotificationRuleTriggerConfig {

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTriggerType.java

@ -22,10 +22,12 @@ import lombok.RequiredArgsConstructor;
public enum NotificationRuleTriggerType {
ALARM(true),
ALARM_COMMENT(true),
DEVICE_INACTIVITY(false),
ENTITY_ACTION(false);
ENTITY_ACTION(false),
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT(false);
@Getter
private final boolean isUpdatable;
private final boolean updatable;
}

41
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2022 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.notification.rule.trigger;
import lombok.Data;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Set;
import java.util.UUID;
@Data
public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<ComponentLifecycleEvent> ruleChainEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events
private boolean onlyRuleChainLifecycleFailures;
private boolean trackRuleNodeEvents;
private Set<ComponentLifecycleEvent> ruleNodeEvents; // available options: STARTED, UPDATED, STOPPED. if empty - all events
private boolean onlyRuleNodeLifecycleFailures;
private Set<UUID> ruleChains; // if empty - all rule chains
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT;
}
}

4
dao/src/main/resources/sql/schema-entities.sql

@ -807,7 +807,7 @@ CREATE TABLE IF NOT EXISTS notification_template (
created_time BIGINT NOT NULL,
tenant_id UUID NULL CONSTRAINT fk_notification_template_tenant_id REFERENCES tenant(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
notification_type VARCHAR(32) NOT NULL,
notification_type VARCHAR(50) NOT NULL,
configuration VARCHAR(10000) NOT NULL,
CONSTRAINT uq_notification_template_name UNIQUE (tenant_id, name)
);
@ -845,7 +845,7 @@ CREATE TABLE IF NOT EXISTS notification (
created_time BIGINT NOT NULL,
request_id UUID NULL CONSTRAINT fk_notification_request_id REFERENCES notification_request(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL CONSTRAINT fk_notification_recipient_id REFERENCES tb_user(id) ON DELETE CASCADE,
type VARCHAR(32) NOT NULL,
type VARCHAR(50) NOT NULL,
subject VARCHAR(255),
text VARCHAR(1000) NOT NULL,
additional_config VARCHAR(1000),

23
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.html

@ -198,6 +198,29 @@
</section>
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ALARM_COMMENT"
[stepControl]="alarmCommentTemplateForm">
<ng-template matStepLabel>{{ 'notification.alarm-comment-trigger-settings' | translate }}</ng-template>
<form [formGroup]="alarmCommentTemplateForm">
<section formGroupName="recipientsConfig">
<tb-entity-list
required
formControlName="targets"
[entityType]="entityType.NOTIFICATION_TARGET"
placeholderText="notification.target">
</tb-entity-list>
</section>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</section>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<mat-divider></mat-divider>

8
ui-ngx/src/app/modules/home/pages/notification-center/rule-table/rule-notification-dialog.component.ts

@ -56,6 +56,7 @@ export class RuleNotificationDialogComponent extends
alarmTemplateForm: FormGroup;
deviceInactivityTemplateForm: FormGroup;
entityActionTemplateForm: FormGroup;
alarmCommentTemplateForm: FormGroup;
triggerType = TriggerType;
triggerTypes: TriggerType[] = Object.values(TriggerType);
@ -154,8 +155,15 @@ export class RuleNotificationDialogComponent extends
})
});
this.alarmCommentTemplateForm = this.fb.group({
recipientsConfig: this.fb.group({
targets: [[], Validators.required]
})
});
this.triggerTypeFormsMap = new Map<TriggerType, FormGroup>([
[TriggerType.ALARM, this.alarmTemplateForm],
[TriggerType.ALARM_COMMENT, this.alarmCommentTemplateForm],
[TriggerType.DEVICE_INACTIVITY, this.deviceInactivityTemplateForm],
[TriggerType.ENTITY_ACTION, this.entityActionTemplateForm]
]);

17
ui-ngx/src/app/shared/models/notification.models.ts

@ -277,7 +277,8 @@ export enum NotificationType {
GENERAL = 'GENERAL',
ALARM = 'ALARM',
DEVICE_INACTIVITY = 'DEVICE_INACTIVITY',
ENTITY_ACTION = 'ENTITY_ACTION'
ENTITY_ACTION = 'ENTITY_ACTION',
ALARM_COMMENT = 'ALARM_COMMENT'
}
interface NotificationTemplateTypeTranslate {
@ -309,17 +310,25 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
name: 'notification.template-type.entity-action',
hint: 'notification.template-hint.entity-action'
}
]
],
[NotificationType.ALARM_COMMENT,
{
name: 'notification.template-type.alarm-comment',
hint: 'notification.template-hint.alarm-comment'
}
],
]);
export enum TriggerType {
ALARM = 'ALARM',
DEVICE_INACTIVITY = 'DEVICE_INACTIVITY',
ENTITY_ACTION = 'ENTITY_ACTION'
ENTITY_ACTION = 'ENTITY_ACTION',
ALARM_COMMENT = 'ALARM_COMMENT'
}
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
[TriggerType.ALARM, 'notification.trigger.alarm'],
[TriggerType.DEVICE_INACTIVITY, 'notification.trigger.device-inactivity'],
[TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action']
[TriggerType.ENTITY_ACTION, 'notification.trigger.entity-action'],
[TriggerType.ALARM_COMMENT, 'notification.trigger.alarm-comment']
]);

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2690,6 +2690,7 @@
"add-rule": "Add rule",
"add-target": "Add recipient",
"add-template": "Add template",
"alarm-comment-trigger-settings": "Alarm comment trigger settings",
"alarm-trigger-settings": "Alarm trigger settings",
"all": "All",
"at-least-one-should-be-selected": "At least one should be selected",
@ -2816,6 +2817,7 @@
"template": "Template",
"template-hint": {
"alarm": "Available params: ${alarmType}, ${alarmSeverity}, ${alarmStatus}, ${alarmOriginatorEntityType}, ${alarmOriginatorId}, ${alarmId}",
"alarm-comment": "Available params: ${comment}, ${alarmType}, ${alarmId}",
"device-inactivity": "Available params: ${deviceName}, ${deviceType}, ${deviceId}",
"entity-action": "Available params: ${actionType}, ${entityType}, ${entityName}, ${entityId}, ${originatorUserName}, ${originatorUserId}",
"general": "Available params: ${recipientEmail}, ${recipientFirstName}, ${recipientLastName}"
@ -2824,6 +2826,7 @@
"template-required": "Template is required",
"template-type": {
"alarm": "Alarm",
"alarm-comment": "Alarm comment",
"device-inactivity": "Device inactivity",
"entity-action": "Entity action",
"general": "General"
@ -2833,6 +2836,7 @@
"time": "Time",
"trigger": {
"alarm": "Alarm",
"alarm-comment": "Alarm comment",
"device-inactivity": "Device inactivity",
"entity-action": "Entity action",
"trigger": "Trigger",

Loading…
Cancel
Save