diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index ae9d9677c7..5dd738b029 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint; +import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.customer.CustomerService; @@ -105,6 +106,9 @@ public class ActorSystemContext { @Autowired @Getter private EventService eventService; + @Autowired + @Getter private AlarmService alarmService; + @Autowired @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint; diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java index f425f8f8b3..4c31fd04d4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java @@ -15,22 +15,27 @@ */ package org.thingsboard.server.actors.rule; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmId; import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.extensions.api.device.DeviceAttributes; import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; import org.thingsboard.server.extensions.api.device.DeviceMetaData; import org.thingsboard.server.extensions.api.rules.RuleContext; import java.util.Optional; +import java.util.concurrent.ExecutionException; public class RuleProcessingContext implements RuleContext { private final TimeseriesService tsService; private final EventService eventService; + private final AlarmService alarmService; private final RuleId ruleId; private TenantId tenantId; private CustomerId customerId; @@ -40,6 +45,7 @@ public class RuleProcessingContext implements RuleContext { RuleProcessingContext(ActorSystemContext systemContext, RuleId ruleId) { this.tsService = systemContext.getTsService(); this.eventService = systemContext.getEventService(); + this.alarmService = systemContext.getAlarmService(); this.ruleId = ruleId; } @@ -77,6 +83,25 @@ public class RuleProcessingContext implements RuleContext { return eventService.findEvent(tenantId, deviceId, eventType, eventUid); } + @Override + public Alarm createOrUpdateAlarm(Alarm alarm) { + alarm.setTenantId(tenantId); + return alarmService.createOrUpdateAlarm(alarm); + } + + public Optional findLatestAlarm(EntityId originator, String alarmType) { + try { + return Optional.ofNullable(alarmService.findLatestByOriginatorAndType(tenantId, originator, alarmType).get()); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to lookup alarm!", e); + } + } + + @Override + public ListenableFuture clearAlarm(AlarmId alarmId, long clearTs) { + return alarmService.clearAlarm(alarmId, clearTs); + } + private void checkEvent(Event event) { if (event.getTenantId() == null) { event.setTenantId(tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 478d5c803d..f9d70fa874 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -110,7 +110,6 @@ public class EntityRelation { if (to != null ? !to.equals(that.to) : that.to != null) return false; if (type != null ? !type.equals(that.type) : that.type != null) return false; return typeGroup == that.typeGroup; - } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 63fba03a52..0c54f717b7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.alarm; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.*; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageData; /** @@ -40,4 +41,6 @@ public interface AlarmService { AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 81f670a096..d52df0f4e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -27,6 +27,7 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.*; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -111,6 +112,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } } + public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmDao.findLatestByOriginatorAndType(tenantId, originator, type); + } + private Alarm createAlarm(Alarm alarm) throws InterruptedException, ExecutionException { log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm); @@ -204,15 +209,15 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ validateId(alarmId, "Incorrect alarmId " + alarmId); return Futures.transform(alarmDao.findAlarmByIdAsync(alarmId.getId()), (AsyncFunction) alarm1 -> { - AlarmInfo alarmInfo = new AlarmInfo(alarm1); - return Futures.transform( - entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function) - originatorName -> { - alarmInfo.setOriginatorName(originatorName); - return alarmInfo; - } - ); - }); + AlarmInfo alarmInfo = new AlarmInfo(alarm1); + return Futures.transform( + entityService.fetchEntityNameAsync(alarmInfo.getOriginator()), (Function) + originatorName -> { + alarmInfo.setOriginatorName(originatorName); + return alarmInfo; + } + ); + }); } @Override @@ -234,7 +239,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ )); } return Futures.successfulAsList(alarmFutures); - }); + }); } return Futures.transform(alarms, new Function, TimePageData>() { @Nullable @@ -247,7 +252,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public AlarmSeverity findHighestAlarmSeverity(EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus) { + AlarmStatus alarmStatus) { TimePageLink nextPageLink = new TimePageLink(100); boolean hasNext = true; AlarmSeverity highestSeverity = null; @@ -321,7 +326,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ List parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList()); for (EntityId parentId : parentEntities) { updateAlarmRelation(parentId, alarm.getId(), oldStatus, newStatus); - } + } updateAlarmRelation(alarm.getOriginator(), alarm.getId(), oldStatus, newStatus); } catch (ExecutionException | InterruptedException e) { log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus); diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java index 73f657635f..6b381bebc0 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java @@ -15,9 +15,12 @@ */ package org.thingsboard.server.extensions.api.rules; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleId; -import org.thingsboard.server.extensions.api.device.DeviceAttributes; import org.thingsboard.server.extensions.api.device.DeviceMetaData; import java.util.Optional; @@ -34,4 +37,9 @@ public interface RuleContext { Optional findEvent(String eventType, String eventUid); + Optional findLatestAlarm(EntityId originator, String alarmType); + + Alarm createOrUpdateAlarm(Alarm alarm); + + ListenableFuture clearAlarm(AlarmId id, long clearTs); } diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java index a4125b3df4..1e4fbf2bc9 100644 --- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java +++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java @@ -18,6 +18,8 @@ package org.thingsboard.server.extensions.api.rules; import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; import org.thingsboard.server.extensions.api.component.ConfigurableComponent; +import javax.script.ScriptException; + /** * @author Andrew Shvayka */ diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java index ed9402520d..8e99807cc9 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java @@ -75,18 +75,4 @@ public abstract class BasicJsFilter implements RuleFilter } } - protected static Object getValue(KvEntry attr) { - switch (attr.getDataType()) { - case STRING: - return attr.getStrValue().get(); - case LONG: - return attr.getLongValue().get(); - case DOUBLE: - return attr.getDoubleValue().get(); - case BOOLEAN: - return attr.getBooleanValue().get(); - } - return null; - } - } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java index a63fcab8e7..323de921bd 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java @@ -16,9 +16,6 @@ package org.thingsboard.server.extensions.core.filter; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.msg.core.UpdateAttributesRequest; import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; import org.thingsboard.server.common.msg.session.FromDeviceMsg; @@ -28,10 +25,6 @@ import org.thingsboard.server.extensions.api.rules.RuleContext; import javax.script.Bindings; import javax.script.ScriptException; -import javax.script.SimpleBindings; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; /** * @author Andrew Shvayka @@ -40,25 +33,18 @@ import java.util.Map; @Slf4j public class DeviceAttributesFilter extends BasicJsFilter { - public static final String CLIENT_SIDE = "cs"; - public static final String SERVER_SIDE = "ss"; - public static final String SHARED = "shared"; - @Override protected boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException { return evaluator.execute(toBindings(ctx.getDeviceMetaData().getDeviceAttributes(), msg != null ? msg.getPayload() : null)); } private Bindings toBindings(DeviceAttributes attributes, FromDeviceMsg msg) { - Bindings bindings = new SimpleBindings(); - convertListEntries(bindings, CLIENT_SIDE, attributes.getClientSideAttributes()); - convertListEntries(bindings, SERVER_SIDE, attributes.getServerSideAttributes()); - convertListEntries(bindings, SHARED, attributes.getServerSidePublicAttributes()); + Bindings bindings = NashornJsEvaluator.getAttributeBindings(attributes); if (msg != null) { switch (msg.getMsgType()) { case POST_ATTRIBUTES_REQUEST: - updateBindings(bindings, (UpdateAttributesRequest) msg); + bindings = NashornJsEvaluator.updateBindings(bindings, (UpdateAttributesRequest) msg); break; } } @@ -66,28 +52,4 @@ public class DeviceAttributesFilter extends BasicJsFilter { return bindings; } - private void updateBindings(Bindings bindings, UpdateAttributesRequest msg) { - Map attrMap = (Map) bindings.get(CLIENT_SIDE); - for (AttributeKvEntry attr : msg.getAttributes()) { - if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey()) - && !SHARED.equalsIgnoreCase(attr.getKey())) { - bindings.put(attr.getKey(), getValue(attr)); - } - attrMap.put(attr.getKey(), getValue(attr)); - } - bindings.put(CLIENT_SIDE, attrMap); - } - - public static Bindings convertListEntries(Bindings bindings, String attributesVarName, Collection attributes) { - Map attrMap = new HashMap<>(); - for (AttributeKvEntry attr : attributes) { - if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey()) - && !SHARED.equalsIgnoreCase(attr.getKey())) { - bindings.put(attr.getKey(), getValue(attr)); - } - attrMap.put(attr.getKey(), getValue(attr)); - } - bindings.put(attributesVarName, attrMap); - return bindings; - } } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java index 8eaa2336c6..d16258e082 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java @@ -23,9 +23,7 @@ import org.thingsboard.server.common.msg.session.FromDeviceMsg; import org.thingsboard.server.extensions.api.component.Filter; import org.thingsboard.server.extensions.api.rules.RuleContext; -import javax.script.Bindings; import javax.script.ScriptException; -import javax.script.SimpleBindings; import java.util.List; /** @@ -41,7 +39,7 @@ public class DeviceTelemetryFilter extends BasicJsFilter { if (deviceMsg instanceof TelemetryUploadRequest) { TelemetryUploadRequest telemetryMsg = (TelemetryUploadRequest) deviceMsg; for (List entries : telemetryMsg.getData().values()) { - if (evaluator.execute(toBindings(entries))) { + if (evaluator.execute(NashornJsEvaluator.toBindings(entries))) { return true; } } @@ -49,12 +47,4 @@ public class DeviceTelemetryFilter extends BasicJsFilter { return false; } - private Bindings toBindings(List entries) { - Bindings bindings = new SimpleBindings(); - for (KvEntry entry : entries) { - bindings.put(entry.getKey(), getValue(entry)); - } - return bindings; - } - } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java index cd3cd07d5f..84b76cad5d 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java @@ -17,10 +17,16 @@ package org.thingsboard.server.extensions.core.filter; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.msg.core.UpdateAttributesRequest; +import org.thingsboard.server.extensions.api.device.DeviceAttributes; import javax.script.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * @author Andrew Shvayka @@ -28,6 +34,9 @@ import javax.script.*; @Slf4j public class NashornJsEvaluator { + public static final String CLIENT_SIDE = "cs"; + public static final String SERVER_SIDE = "ss"; + public static final String SHARED = "shared"; private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); private CompiledScript engine; @@ -47,6 +56,65 @@ public class NashornJsEvaluator { } } + public static Bindings convertListEntries(Bindings bindings, String attributesVarName, Collection attributes) { + Map attrMap = new HashMap<>(); + for (AttributeKvEntry attr : attributes) { + if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey()) + && !SHARED.equalsIgnoreCase(attr.getKey())) { + bindings.put(attr.getKey(), getValue(attr)); + } + attrMap.put(attr.getKey(), getValue(attr)); + } + bindings.put(attributesVarName, attrMap); + return bindings; + } + + public static Bindings updateBindings(Bindings bindings, UpdateAttributesRequest msg) { + Map attrMap = (Map) bindings.get(CLIENT_SIDE); + for (AttributeKvEntry attr : msg.getAttributes()) { + if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey()) + && !SHARED.equalsIgnoreCase(attr.getKey())) { + bindings.put(attr.getKey(), getValue(attr)); + } + attrMap.put(attr.getKey(), getValue(attr)); + } + bindings.put(CLIENT_SIDE, attrMap); + return bindings; + } + + protected static Object getValue(KvEntry attr) { + switch (attr.getDataType()) { + case STRING: + return attr.getStrValue().get(); + case LONG: + return attr.getLongValue().get(); + case DOUBLE: + return attr.getDoubleValue().get(); + case BOOLEAN: + return attr.getBooleanValue().get(); + } + return null; + } + + public static Bindings toBindings(List entries) { + return toBindings(new SimpleBindings(), entries); + } + + public static Bindings toBindings(Bindings bindings, List entries) { + for (KvEntry entry : entries) { + bindings.put(entry.getKey(), getValue(entry)); + } + return bindings; + } + + public static Bindings getAttributeBindings(DeviceAttributes attributes) { + Bindings bindings = new SimpleBindings(); + convertListEntries(bindings, CLIENT_SIDE, attributes.getClientSideAttributes()); + convertListEntries(bindings, SERVER_SIDE, attributes.getServerSideAttributes()); + convertListEntries(bindings, SHARED, attributes.getServerSidePublicAttributes()); + return bindings; + } + public Boolean execute(Bindings bindings) throws ScriptException { Object eval = engine.eval(bindings); if (eval instanceof Boolean) { diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java index d7ebfec64e..4a39ee1449 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java @@ -32,7 +32,7 @@ import java.util.Optional; /** * @author Andrew Shvayka */ -@Processor(name = "Alarm Deduplication Processor", descriptor = "AlarmDeduplicationProcessorDescriptor.json", +@Processor(name = "(Deprecated) Alarm Deduplication Processor", descriptor = "AlarmDeduplicationProcessorDescriptor.json", configuration = AlarmDeduplicationProcessorConfiguration.class) @Slf4j public class AlarmDeduplicationProcessor extends SimpleRuleLifecycleComponent diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessor.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessor.java new file mode 100644 index 0000000000..3dec45e482 --- /dev/null +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessor.java @@ -0,0 +1,213 @@ +/** + * Copyright © 2016-2017 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.extensions.core.processor; + +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.runtime.parser.ParseException; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.msg.core.TelemetryUploadRequest; +import org.thingsboard.server.common.msg.core.UpdateAttributesRequest; +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg; +import org.thingsboard.server.common.msg.session.FromDeviceMsg; +import org.thingsboard.server.extensions.api.component.Processor; +import org.thingsboard.server.extensions.api.rules.*; +import org.thingsboard.server.extensions.core.filter.NashornJsEvaluator; +import org.thingsboard.server.extensions.core.utils.VelocityUtils; + +import javax.script.Bindings; +import javax.script.ScriptException; +import java.io.IOException; +import java.util.List; + +/** + * @author Andrew Shvayka + */ +@Processor(name = "Alarm Processor", descriptor = "AlarmProcessorDescriptor.json", + configuration = AlarmProcessorConfiguration.class) +@Slf4j +public class AlarmProcessor implements RuleProcessor { + + static final String IS_NEW_ALARM = "isNewAlarm"; + static final String IS_EXISTING_ALARM = "isExistingAlarm"; + static final String IS_CLEARED_ALARM = "isClearedAlarm"; + + protected NashornJsEvaluator newAlarmEvaluator; + protected NashornJsEvaluator clearAlarmEvaluator; + + private ObjectMapper mapper = new ObjectMapper(); + private AlarmProcessorConfiguration configuration; + private AlarmStatus status; + private AlarmSeverity severity; + private Template alarmTypeTemplate; + private Template alarmDetailsTemplate; + + + @Override + public void init(AlarmProcessorConfiguration configuration) { + this.configuration = configuration; + try { + this.alarmTypeTemplate = VelocityUtils.create(configuration.getAlarmTypeTemplate(), "Alarm Type Template"); + this.alarmDetailsTemplate = VelocityUtils.create(configuration.getAlarmDetailsTemplate(), "Alarm Details Template"); + this.status = AlarmStatus.valueOf(configuration.getAlarmStatus()); + this.severity = AlarmSeverity.valueOf(configuration.getAlarmSeverity()); + initEvaluators(); + } catch (Exception e) { + log.error("Failed to create templates based on provided configuration!", e); + throw new RuntimeException("Failed to create templates based on provided configuration!", e); + } + } + + @Override + public void resume() { + initEvaluators(); + log.debug("Resume method was called, but no impl provided!"); + } + + @Override + public void suspend() { + destroyEvaluators(); + log.debug("Suspend method was called, but no impl provided!"); + } + + @Override + public void stop() { + destroyEvaluators(); + log.debug("Stop method was called, but no impl provided!"); + } + + @Override + public RuleProcessingMetaData process(RuleContext ctx, ToDeviceActorMsg wrapper) throws RuleException { + RuleProcessingMetaData md = new RuleProcessingMetaData(); + + FromDeviceMsg msg = wrapper.getPayload(); + Bindings bindings = buildBindings(ctx, msg); + + boolean isActiveAlarm; + boolean isClearedAlarm; + + try { + isActiveAlarm = newAlarmEvaluator.execute(bindings); + isClearedAlarm = clearAlarmEvaluator.execute(bindings); + } catch (ScriptException e) { + log.debug("[{}] Failed to evaluate alarm expressions!", ctx.getRuleId(), e); + throw new RuleException("Failed to evaluate alarm expressions!", e); + } + + if (!isActiveAlarm && !isClearedAlarm) { + log.debug("[{}] Incoming message do not trigger alarm", ctx.getRuleId()); + return md; + } + + Alarm existing = null; + if (isActiveAlarm) { + Alarm alarm = buildAlarm(ctx, msg); + existing = ctx.createOrUpdateAlarm(alarm); + if (existing.getStartTs() == alarm.getStartTs()) { + log.debug("[{}][{}] New Active Alarm detected"); + md.put(IS_NEW_ALARM, Boolean.TRUE); + } else { + log.debug("[{}][{}] Existing Active Alarm detected"); + md.put(IS_EXISTING_ALARM, Boolean.TRUE); + } + } else if (isClearedAlarm) { + VelocityContext context = VelocityUtils.createContext(ctx.getDeviceMetaData(), msg); + String alarmType = VelocityUtils.merge(alarmTypeTemplate, context); + Optional alarm = ctx.findLatestAlarm(ctx.getDeviceMetaData().getDeviceId(), alarmType); + if (alarm.isPresent()) { + ctx.clearAlarm(alarm.get().getId(), System.currentTimeMillis()); + log.debug("[{}][{}] Existing Active Alarm cleared"); + md.put(IS_CLEARED_ALARM, Boolean.TRUE); + existing = alarm.get(); + } + } + //TODO: handle cleared alarms + + if (existing != null) { + md.put("alarmId", existing.getId().getId()); + md.put("alarmType", existing.getType()); + md.put("alarmSeverity", existing.getSeverity()); + try { + md.put("alarmDetails", mapper.writeValueAsString(existing.getDetails())); + } catch (JsonProcessingException e) { + throw new RuleException("Failed to serialize alarm details", e); + } + } + + return md; + } + + private Alarm buildAlarm(RuleContext ctx, FromDeviceMsg msg) throws RuleException { + VelocityContext context = VelocityUtils.createContext(ctx.getDeviceMetaData(), msg); + String alarmType = VelocityUtils.merge(alarmTypeTemplate, context); + String alarmDetails = VelocityUtils.merge(alarmDetailsTemplate, context); + + Alarm alarm = new Alarm(); + alarm.setOriginator(ctx.getDeviceMetaData().getDeviceId()); + alarm.setType(alarmType); + + alarm.setStatus(status); + alarm.setSeverity(severity); + alarm.setPropagate(configuration.isAlarmPropagateFlag()); + + try { + alarm.setDetails(mapper.readTree(alarmDetails)); + } catch (IOException e) { + log.debug("[{}] Failed to parse alarm details {} as json string after evaluation.", ctx.getRuleId(), e); + throw new RuleException("Failed to parse alarm details as json string after evaluation!", e); + } + return alarm; + } + + private Bindings buildBindings(RuleContext ctx, FromDeviceMsg msg) { + Bindings bindings = NashornJsEvaluator.getAttributeBindings(ctx.getDeviceMetaData().getDeviceAttributes()); + if (msg != null) { + switch (msg.getMsgType()) { + case POST_ATTRIBUTES_REQUEST: + bindings = NashornJsEvaluator.updateBindings(bindings, (UpdateAttributesRequest) msg); + break; + case POST_TELEMETRY_REQUEST: + TelemetryUploadRequest telemetryMsg = (TelemetryUploadRequest) msg; + for (List entries : telemetryMsg.getData().values()) { + bindings = NashornJsEvaluator.toBindings(bindings, entries); + } + } + } + return bindings; + } + + private void initEvaluators() { + newAlarmEvaluator = new NashornJsEvaluator(configuration.getNewAlarmExpression()); + clearAlarmEvaluator = new NashornJsEvaluator(configuration.getClearAlarmExpression()); + } + + private void destroyEvaluators() { + if (newAlarmEvaluator != null) { + newAlarmEvaluator.destroy(); + } + if (clearAlarmEvaluator != null) { + clearAlarmEvaluator.destroy(); + } + } +} diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessorConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessorConfiguration.java new file mode 100644 index 0000000000..197cc44acc --- /dev/null +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmProcessorConfiguration.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2017 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.extensions.core.processor; + +import lombok.Data; + +import java.util.List; + +/** + * @author Andrew Shvayka + */ +@Data +public class AlarmProcessorConfiguration { + + private String newAlarmExpression; + private String clearAlarmExpression; + + private String alarmTypeTemplate; + private String alarmSeverity; + private String alarmStatus; + private boolean alarmPropagateFlag; + + private String alarmDetailsTemplate; + +} \ No newline at end of file diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java index 6844234661..fbb7612c68 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.msg.session.FromDeviceMsg; import org.thingsboard.server.extensions.api.device.DeviceAttributes; import org.thingsboard.server.extensions.api.device.DeviceMetaData; import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData; -import org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter; +import org.thingsboard.server.extensions.core.filter.NashornJsEvaluator; import java.io.StringReader; import java.io.StringWriter; @@ -70,9 +70,9 @@ public class VelocityUtils { context.put("date", new DateTool()); DeviceAttributes deviceAttributes = deviceMetaData.getDeviceAttributes(); - pushAttributes(context, deviceAttributes.getClientSideAttributes(), DeviceAttributesFilter.CLIENT_SIDE); - pushAttributes(context, deviceAttributes.getServerSideAttributes(), DeviceAttributesFilter.SERVER_SIDE); - pushAttributes(context, deviceAttributes.getServerSidePublicAttributes(), DeviceAttributesFilter.SHARED); + pushAttributes(context, deviceAttributes.getClientSideAttributes(), NashornJsEvaluator.CLIENT_SIDE); + pushAttributes(context, deviceAttributes.getServerSideAttributes(), NashornJsEvaluator.SERVER_SIDE); + pushAttributes(context, deviceAttributes.getServerSidePublicAttributes(), NashornJsEvaluator.SHARED); switch (payload.getMsgType()) { case POST_TELEMETRY_REQUEST: diff --git a/extensions-core/src/main/resources/AlarmProcessorDescriptor.json b/extensions-core/src/main/resources/AlarmProcessorDescriptor.json new file mode 100644 index 0000000000..32a4a50010 --- /dev/null +++ b/extensions-core/src/main/resources/AlarmProcessorDescriptor.json @@ -0,0 +1,113 @@ +{ + "schema": { + "title": "Alarm Configuration", + "type": "object", + "properties": { + "newAlarmExpression": { + "title": "Alarm trigger expression", + "type": "string", + "default": "" + }, + "clearAlarmExpression": { + "title": "Alarm clear expression", + "type": "string", + "default": "" + }, + "alarmTypeTemplate": { + "title": "Alarm type", + "type": "string" + }, + "alarmSeverity": { + "title": "Severity", + "type": "string" + }, + "alarmStatus": { + "title": "Status", + "type": "string" + }, + "alarmPropagateFlag": { + "title": "Propagate Alarm", + "type": "boolean" + }, + "alarmDetailsTemplate": { + "title": "Alarm details", + "type": "string" + } + }, + "required": [ + "newAlarmExpression", + "clearAlarmExpression", + "alarmSeverity", + "alarmStatus", + "alarmTypeTemplate", + "alarmDetailsTemplate" + ] + }, + "form": [ + { + "key": "newAlarmExpression", + "type": "javascript" + }, + { + "key": "clearAlarmExpression", + "type": "javascript" + }, + { + "key": "alarmSeverity", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "CRITICAL", + "label": "Critical" + }, + { + "value": "MAJOR", + "label": "Major" + }, + { + "value": "MINOR", + "label": "Minor" + }, + { + "value": "WARNING", + "label": "Warning" + }, + { + "value": "INDETERMINATE", + "label": "Indeterminate" + } + ] + }, + { + "key": "alarmStatus", + "type": "rc-select", + "multiple": false, + "items": [ + { + "value": "ACTIVE_UNACK", + "label": "Active Unacknowledged" + }, + { + "value": "ACTIVE_ACK", + "label": "Active Acknowledged" + }, + { + "value": "CLEARED_UNACK", + "label": "Cleared Unacknowledged" + }, + { + "value": "CLEARED_ACK", + "label": "Cleared Acknowledged" + } + ] + }, + "alarmTypeTemplate", + "alarmPropagateFlag", + { + "key": "alarmDetailsTemplate", + "type": "textarea", + "rows": 5 + } + ] +} \ No newline at end of file