From 2dec050a378632082018fb70a6be39fe859edfe3 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 11 May 2018 18:53:50 +0300 Subject: [PATCH 1/5] Separate Alarm node to Create and Clear Alarm nodes. --- .../server/controller/AlarmController.java | 2 +- .../server/dao/alarm/AlarmService.java | 3 +- .../server/dao/alarm/BaseAlarmService.java | 6 +- .../dao/service/BaseAlarmServiceTest.java | 2 +- .../telemetry/sub/SubscriptionUpdate.java | 14 +- .../engine/action/TbAbstractAlarmNode.java | 117 ++++++++++ .../TbAbstractAlarmNodeConfiguration.java | 26 +++ .../rule/engine/action/TbAlarmNode.java | 219 ------------------ .../rule/engine/action/TbClearAlarmNode.java | 80 +++++++ .../action/TbClearAlarmNodeConfiguration.java | 36 +++ .../rule/engine/action/TbCreateAlarmNode.java | 106 +++++++++ ...va => TbCreateAlarmNodeConfiguration.java} | 19 +- .../filter/TbJsFilterNodeConfiguration.java | 2 +- .../static/rulenode/rulenode-core-config.js | 6 +- .../rule/engine/action/TbAlarmNodeTest.java | 98 ++++---- ui/src/app/alarm/alarm-table.directive.js | 4 +- ui/src/app/alarm/alarm-table.tpl.html | 7 + ui/src/app/locale/locale.constant.js | 3 +- ui/src/app/rulechain/rulechain.controller.js | 2 +- ui/src/app/rulechain/rulechain.scss | 13 +- ui/src/app/rulechain/rulechain.tpl.html | 11 + 21 files changed, 470 insertions(+), 306 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java delete mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java rename rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/{TbAlarmNodeConfiguration.java => TbCreateAlarmNodeConfiguration.java} (63%) diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index 81bcf7e765..ace024cb0f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -93,7 +93,7 @@ public class AlarmController extends BaseController { try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); checkAlarmId(alarmId); - alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get(); + alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get(); } catch (Exception e) { throw handleException(e); } 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 4da7df22d9..8d73fce497 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.*; import org.thingsboard.server.common.data.id.EntityId; @@ -30,7 +31,7 @@ public interface AlarmService { ListenableFuture ackAlarm(AlarmId alarmId, long ackTs); - ListenableFuture clearAlarm(AlarmId alarmId, long ackTs); + ListenableFuture clearAlarm(AlarmId alarmId, JsonNode details, long ackTs); ListenableFuture findAlarmByIdAsync(AlarmId alarmId); 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 e669ae226b..c0ca37a715 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Function; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; @@ -187,7 +188,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public ListenableFuture clearAlarm(AlarmId alarmId, long clearTime) { + public ListenableFuture clearAlarm(AlarmId alarmId, JsonNode details, long clearTime) { return getAndUpdate(alarmId, new Function() { @Nullable @Override @@ -199,6 +200,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; alarm.setStatus(newStatus); alarm.setClearTs(clearTime); + if (details != null) { + alarm.setDetails(details); + } alarmDao.save(alarm); updateRelations(alarm, oldStatus, newStatus); return true; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index fd071e9c51..9ef8457438 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -168,7 +168,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get(); + alarmService.clearAlarm(created.getId(), null, System.currentTimeMillis()).get(); created = alarmService.findAlarmByIdAsync(created.getId()).get(); alarms = alarmService.findAlarms(AlarmQuery.builder() diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java index a9a3a3b65f..7a19e43db9 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java @@ -31,12 +31,14 @@ public class SubscriptionUpdate { super(); this.subscriptionId = subscriptionId; this.data = new TreeMap<>(); - for (TsKvEntry tsEntry : data) { - List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); - Object[] value = new Object[2]; - value[0] = tsEntry.getTs(); - value[1] = tsEntry.getValueAsString(); - values.add(value); + if (data != null) { + for (TsKvEntry tsEntry : data) { + List values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); + Object[] value = new Object[2]; + value[0] = tsEntry.getTs(); + value[1] = tsEntry.getValueAsString(); + values.add(value); + } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java new file mode 100644 index 0000000000..b5a5982031 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2018 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.rule.engine.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.*; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import static org.thingsboard.rule.engine.DonAsynchron.withCallback; + +@Slf4j +public abstract class TbAbstractAlarmNode implements TbNode { + + static final String PREV_ALARM_DETAILS = "prevAlarmDetails"; + + static final String IS_NEW_ALARM = "isNewAlarm"; + static final String IS_EXISTING_ALARM = "isExistingAlarm"; + static final String IS_CLEARED_ALARM = "isClearedAlarm"; + + private final ObjectMapper mapper = new ObjectMapper(); + + protected C config; + private ScriptEngine buildDetailsJsEngine; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = loadAlarmNodeConfig(configuration); + this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details"); + } + + protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + withCallback(processAlarm(ctx, msg), + alarmResult -> { + if (alarmResult.alarm == null) { + ctx.tellNext(msg, "False"); + } else if (alarmResult.isCreated) { + ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created"); + } else if (alarmResult.isUpdated) { + ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated"); + } else if (alarmResult.isCleared) { + ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Cleared"); + } + }, + t -> ctx.tellError(msg, t)); + } + + protected abstract ListenableFuture processAlarm(TbContext ctx, TbMsg msg); + + protected ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) { + return ctx.getJsExecutor().executeAsync(() -> { + TbMsg theMsg = msg; + if (previousDetails != null) { + TbMsgMetaData metaData = msg.getMetaData().copy(); + metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails)); + theMsg = ctx.newMsg(msg.getType(), msg.getOriginator(), metaData, msg.getData()); + } + return buildDetailsJsEngine.executeJson(theMsg); + }); + } + + private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) { + JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm); + String data = jsonNodes.toString(); + TbMsgMetaData metaData = originalMsg.getMetaData().copy(); + if (alarmResult.isCreated) { + metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isUpdated) { + metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString()); + } else if (alarmResult.isCleared) { + metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString()); + } + return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data); + } + + + @Override + public void destroy() { + if (buildDetailsJsEngine != null) { + buildDetailsJsEngine.destroy(); + } + } + + protected static class AlarmResult { + boolean isCreated; + boolean isUpdated; + boolean isCleared; + Alarm alarm; + + AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { + this.isCreated = isCreated; + this.isUpdated = isUpdated; + this.isCleared = isCleared; + this.alarm = alarm; + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java new file mode 100644 index 0000000000..ad1db0ab48 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2018 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.rule.engine.action; + +import lombok.Data; + +@Data +public abstract class TbAbstractAlarmNodeConfiguration { + + private String alarmType; + private String alarmDetailsBuildJs; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java deleted file mode 100644 index e1112971f5..0000000000 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright © 2016-2018 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.rule.engine.action; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Function; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; -import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; - -import static org.thingsboard.rule.engine.DonAsynchron.withCallback; - -@Slf4j -@RuleNode( - type = ComponentType.ACTION, - name = "alarm", relationTypes = {"Created", "Updated", "Cleared", "False"}, - configClazz = TbAlarmNodeConfiguration.class, - nodeDescription = "Create/Update/Clear Alarm", - nodeDetails = "isAlarm - JS function that verifies if Alarm should be CREATED for incoming message.\n" + - "isCleared - JS function that verifies if Alarm should be CLEARED for incoming message.\n" + - "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + - "Node output:\n" + - "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm/isClearedAlarm' " + - "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;" + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", - uiResources = {"static/rulenode/rulenode-core-config.js"}, - configDirective = "tbActionNodeAlarmConfig", - icon = "notifications_active" -) - -public class TbAlarmNode implements TbNode { - - static final String IS_NEW_ALARM = "isNewAlarm"; - static final String IS_EXISTING_ALARM = "isExistingAlarm"; - static final String IS_CLEARED_ALARM = "isClearedAlarm"; - - private final ObjectMapper mapper = new ObjectMapper(); - - private TbAlarmNodeConfiguration config; - private ScriptEngine createJsEngine; - private ScriptEngine clearJsEngine; - private ScriptEngine buildDetailsJsEngine; - - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbAlarmNodeConfiguration.class); - this.createJsEngine = ctx.createJsScriptEngine(config.getCreateConditionJs(), "isAlarm"); - this.clearJsEngine = ctx.createJsScriptEngine(config.getClearConditionJs(), "isCleared"); - this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details"); - } - - @Override - public void onMsg(TbContext ctx, TbMsg msg) { - ListeningExecutor jsExecutor = ctx.getJsExecutor(); - - ListenableFuture shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg)); - ListenableFuture transform = Futures.transform(shouldCreate, (AsyncFunction) create -> { - if (create) { - return createOrUpdate(ctx, msg); - } else { - return checkForClearIfExist(ctx, msg); - } - }, ctx.getDbCallbackExecutor()); - - withCallback(transform, - alarmResult -> { - if (alarmResult.alarm == null) { - ctx.tellNext(msg, "False"); - } else if (alarmResult.isCreated) { - ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created"); - } else if (alarmResult.isUpdated) { - ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated"); - } else if (alarmResult.isCleared) { - ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Cleared"); - } - }, - t -> ctx.tellError(msg, t)); - - } - - private ListenableFuture createOrUpdate(TbContext ctx, TbMsg msg) { - ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); - return Futures.transform(latest, (AsyncFunction) a -> { - if (a == null || a.getStatus().isCleared()) { - return createNewAlarm(ctx, msg); - } else { - return updateAlarm(ctx, msg, a); - } - }, ctx.getDbCallbackExecutor()); - } - - private ListenableFuture checkForClearIfExist(TbContext ctx, TbMsg msg) { - ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); - return Futures.transform(latest, (AsyncFunction) a -> { - if (a != null && !a.getStatus().isCleared()) { - return clearAlarm(ctx, msg, a); - } - return Futures.immediateFuture(new AlarmResult(false, false, false, null)); - }, ctx.getDbCallbackExecutor()); - } - - private ListenableFuture createNewAlarm(TbContext ctx, TbMsg msg) { - ListenableFuture asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg), - (Function) details -> buildAlarm(msg, details, ctx.getTenantId())); - ListenableFuture asyncCreated = Futures.transform(asyncAlarm, - (Function) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); - return Futures.transform(asyncCreated, (Function) alarm -> new AlarmResult(true, false, false, alarm)); - } - - private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { - ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function) details -> { - alarm.setSeverity(config.getSeverity()); - alarm.setPropagate(config.isPropagate()); - alarm.setDetails(details); - alarm.setEndTs(System.currentTimeMillis()); - return ctx.getAlarmService().createOrUpdateAlarm(alarm); - }, ctx.getDbCallbackExecutor()); - - return Futures.transform(asyncUpdated, (Function) a -> new AlarmResult(false, true, false, a)); - } - - private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { - ListenableFuture shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg)); - return Futures.transform(shouldClear, (AsyncFunction) clear -> { - if (clear) { - ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis()); - return Futures.transform(clearFuture, (Function) cleared -> { - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - return new AlarmResult(false, false, true, alarm); - }); - } - return Futures.immediateFuture(new AlarmResult(false, false, false, null)); - }); - } - - private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { - return Alarm.builder() - .tenantId(tenantId) - .originator(msg.getOriginator()) - .status(AlarmStatus.ACTIVE_UNACK) - .severity(config.getSeverity()) - .propagate(config.isPropagate()) - .type(config.getAlarmType()) - //todo-vp: alarm date should be taken from Message or current Time should be used? -// .startTs(System.currentTimeMillis()) -// .endTs(System.currentTimeMillis()) - .details(details) - .build(); - } - - private ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg) { - return ctx.getJsExecutor().executeAsync(() -> buildDetailsJsEngine.executeJson(msg)); - } - - private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) { - JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm); - String data = jsonNodes.toString(); - TbMsgMetaData metaData = originalMsg.getMetaData().copy(); - if (alarmResult.isCreated) { - metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString()); - } else if (alarmResult.isUpdated) { - metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString()); - } else if (alarmResult.isCleared) { - metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString()); - } - return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data); - } - - - @Override - public void destroy() { - if (createJsEngine != null) { - createJsEngine.destroy(); - } - if (clearJsEngine != null) { - clearJsEngine.destroy(); - } - if (buildDetailsJsEngine != null) { - buildDetailsJsEngine.destroy(); - } - } - - private static class AlarmResult { - boolean isCreated; - boolean isUpdated; - boolean isCleared; - Alarm alarm; - - AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { - this.isCreated = isCreated; - this.isUpdated = isUpdated; - this.isCleared = isCleared; - this.alarm = alarm; - } - } -} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java new file mode 100644 index 0000000000..ce509fcbb3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2018 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.rule.engine.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.TbNodeUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "clear alarm", relationTypes = {"Cleared", "False"}, + configClazz = TbClearAlarmNodeConfiguration.class, + nodeDescription = "Clear Alarm", + nodeDetails = + "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + + "Node output:\n" + + "If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains 'isClearedAlarm' property " + + "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;" + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeClearAlarmConfig", + icon = "notifications_active" +) +public class TbClearAlarmNode extends TbAbstractAlarmNode { + + @Override + protected TbClearAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException { + return TbNodeUtils.convert(configuration, TbClearAlarmNodeConfiguration.class); + } + + @Override + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { + ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); + return Futures.transform(latest, (AsyncFunction) a -> { + if (a != null && !a.getStatus().isCleared()) { + return clearAlarm(ctx, msg, a); + } + return Futures.immediateFuture(new AlarmResult(false, false, false, null)); + }, ctx.getDbCallbackExecutor()); + } + + private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); + return Futures.transform(asyncDetails, (AsyncFunction) details -> { + ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), details, System.currentTimeMillis()); + return Futures.transform(clearFuture, (AsyncFunction) cleared -> { + if (cleared && details != null) { + alarm.setDetails(details); + } + alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); + return Futures.immediateFuture(new AlarmResult(false, false, true, alarm)); + }); + }, ctx.getDbCallbackExecutor()); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java new file mode 100644 index 0000000000..f3f3072333 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2018 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.rule.engine.action; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; + +@Data +public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration { + + @Override + public TbClearAlarmNodeConfiguration defaultConfiguration() { + TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration(); + configuration.setAlarmDetailsBuildJs("var details = {};\n" + + "if (metadata.prevAlarmDetails) {\n" + + " details = JSON.parse(metadata.prevAlarmDetails);\n" + + "}\n" + + "return details;"); + configuration.setAlarmType("General Alarm"); + return configuration; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java new file mode 100644 index 0000000000..5c2109b399 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2018 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.rule.engine.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.TbNodeUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +@Slf4j +@RuleNode( + type = ComponentType.ACTION, + name = "create alarm", relationTypes = {"Created", "Updated", "False"}, + configClazz = TbCreateAlarmNodeConfiguration.class, + nodeDescription = "Create or Update Alarm", + nodeDetails = + "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + + "Node output:\n" + + "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm' " + + "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;" + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbActionNodeCreateAlarmConfig", + icon = "notifications_active" +) +public class TbCreateAlarmNode extends TbAbstractAlarmNode { + + @Override + protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException { + return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class); + } + + @Override + protected ListenableFuture processAlarm(TbContext ctx, TbMsg msg) { + ListenableFuture latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType()); + return Futures.transform(latest, (AsyncFunction) a -> { + if (a == null || a.getStatus().isCleared()) { + return createNewAlarm(ctx, msg); + } else { + return updateAlarm(ctx, msg, a); + } + }, ctx.getDbCallbackExecutor()); + + } + + private ListenableFuture createNewAlarm(TbContext ctx, TbMsg msg) { + ListenableFuture asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null), + (Function) details -> buildAlarm(msg, details, ctx.getTenantId())); + ListenableFuture asyncCreated = Futures.transform(asyncAlarm, + (Function) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); + return Futures.transform(asyncCreated, (Function) alarm -> new AlarmResult(true, false, false, alarm)); + } + + private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function) details -> { + alarm.setSeverity(config.getSeverity()); + alarm.setPropagate(config.isPropagate()); + alarm.setDetails(details); + alarm.setEndTs(System.currentTimeMillis()); + return ctx.getAlarmService().createOrUpdateAlarm(alarm); + }, ctx.getDbCallbackExecutor()); + + return Futures.transform(asyncUpdated, (Function) a -> new AlarmResult(false, true, false, a)); + } + + private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { + return Alarm.builder() + .tenantId(tenantId) + .originator(msg.getOriginator()) + .status(AlarmStatus.ACTIVE_UNACK) + .severity(config.getSeverity()) + .propagate(config.isPropagate()) + .type(config.getAlarmType()) + //todo-vp: alarm date should be taken from Message or current Time should be used? +// .startTs(System.currentTimeMillis()) +// .endTs(System.currentTimeMillis()) + .details(details) + .build(); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java similarity index 63% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java index 3575459854..b4247947ec 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java @@ -20,22 +20,19 @@ import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @Data -public class TbAlarmNodeConfiguration implements NodeConfiguration { +public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration { - private String createConditionJs; - private String clearConditionJs; - private String alarmDetailsBuildJs; - private String alarmType; private AlarmSeverity severity; private boolean propagate; - @Override - public TbAlarmNodeConfiguration defaultConfiguration() { - TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration(); - configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;"); - configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;"); - configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;"); + public TbCreateAlarmNodeConfiguration defaultConfiguration() { + TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration(); + configuration.setAlarmDetailsBuildJs("var details = {};\n" + + "if (metadata.prevAlarmDetails) {\n" + + " details = JSON.parse(metadata.prevAlarmDetails);\n" + + "}\n"+ + "return details;"); configuration.setAlarmType("General Alarm"); configuration.setSeverity(AlarmSeverity.CRITICAL); configuration.setPropagate(false); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java index 9ab74e890f..51223314b6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java @@ -26,7 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration 20;"); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index dea75a315f..373112ebdd 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1,4 +1,4 @@ -!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(62)},function(e,t){},1,1,1,function(e,t){e.exports="
{{ 'tb.rulenode.test-condition-function' | translate }}
{{ 'tb.rulenode.test-condition-function' | translate }}
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"; -},19,function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testConditionJs=function(e,n){var i=angular.copy(n?r.configuration.createConditionJs:r.configuration.clearConditionJs),o={temperature:22.4,humidity:78},l={sensorType:"temperature"};a.testNodeScript(e,i,"filter",t.instant("tb.rulenode.condition")+"",n?"isAlarm":"isCleared",["msg","metadata","msgType"],o,l,"POST_TELEMETRY").then(function(e){n?r.configuration.createConditionJs=e:r.configuration.clearConditionJs=e,s.$setDirty()})},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i,o,"DebugMsg").then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(45),i=a(r),o=n(32),l=a(o),s=n(33),u=a(s),d=n(31),c=a(d),m=n(36),g=a(m),p=n(40),f=a(p),b=n(41),v=a(b),y=n(39),q=a(y),T=n(35),h=a(T),$=n(43),k=a($),w=n(44),C=a(w),x=n(38),_=a(x),M=n(37),S=a(M),E=n(42),V=a(E);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeAlarmConfig",c.default).directive("tbActionNodeLogConfig",g.default).directive("tbActionNodeRpcReplyConfig",f.default).directive("tbActionNodeRpcRequestConfig",v.default).directive("tbActionNodeRestApiCallConfig",q.default).directive("tbActionNodeKafkaConfig",h.default).directive("tbActionNodeSnsConfig",k.default).directive("tbActionNodeSqsConfig",C.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",S.default).directive("tbActionNodeSendEmailConfig",V.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(18),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,r){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(49),i=a(r),o=n(50),l=a(o),s=n(47),u=a(s),d=n(51),c=a(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",l.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(54),i=a(r),o=n(53),l=a(o),s=n(55),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(26),o=a(i);n(4)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(27),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(28),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(58),i=a(r),o=n(60),l=a(o),s=n(61),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(65),i=a(r),o=n(52),l=a(o),s=n(48),u=a(s),d=n(59),c=a(d),m=n(34),g=a(m),p=n(46),f=a(p),b=n(57),v=a(b),y=n(56),q=a(y),T=n(64),h=a(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbKvMapConfig",q.default).config(h.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping", -"source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method",headers:"Headers",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"SMTP protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout (msec)","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,o.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(63),o=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); +!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),r=e[t[0]];return function(e,t,a){r.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(64)},function(e,t){},1,1,1,function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"; +},20,function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(5),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i,o,"DebugMsg").then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(47),i=r(a),o=n(32),l=r(o),s=n(35),u=r(s),d=n(34),c=r(d),m=n(33),g=r(m),p=n(38),f=r(p),b=n(42),v=r(b),y=n(43),q=r(y),T=n(41),h=r(T),$=n(37),k=r($),w=n(45),C=r(w),x=n(46),_=r(x),M=n(40),S=r(M),E=n(39),V=r(E),N=n(44),P=r(N);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",h.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",_.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",V.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$mdExpansionPanel=t,r.ruleNodeTypes=n,r.credentialsTypeChanged=function(){var e=r.configuration.credentials.type;r.configuration.credentials={},r.configuration.credentials.type=e,r.updateValidity()},r.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){r.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(r.configuration.credentials.caCertFileName=e.name,r.configuration.credentials.caCert=a),"privateKey"==t&&(r.configuration.credentials.privateKeyFileName=e.name,r.configuration.credentials.privateKey=a),"Cert"==t&&(r.configuration.credentials.certFileName=e.name,r.configuration.credentials.cert=a)),r.updateValidity()}})},n.readAsText(e.file)},r.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(r.configuration.credentials.caCertFileName=null,r.configuration.credentials.caCert=null),"privateKey"==e&&(r.configuration.credentials.privateKeyFileName=null,r.configuration.credentials.privateKey=null),"Cert"==e&&(r.configuration.credentials.certFileName=null,r.configuration.credentials.cert=null),r.updateValidity()},r.updateValidity=function(){var e=!0,t=r.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(15),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(19),o=r(i)},function(e,t){"use strict";function n(e){var t=function(t,n,r,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(20),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(51),i=r(a),o=n(52),l=r(o),s=n(49),u=r(s),d=n(53),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",l.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(21),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(22),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(23),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(56),i=r(a),o=n(55),l=r(o),s=n(57),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(27),o=r(i);n(4)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(28),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(29),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(60),i=r(a),o=n(62),l=r(o),s=n(63),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(30),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(31),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(67),i=r(a),o=n(54),l=r(o),s=n(50),u=r(s),d=n(61),c=r(d),m=n(36),g=r(m),p=n(48),f=r(p),b=n(59),v=r(b),y=n(58),q=r(y),T=n(66),h=r(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbKvMapConfig",q.default).config(h.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found", +"no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method",headers:"Headers",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"SMTP protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout (msec)","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){(0,o.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}a.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(65),o=r(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); //# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 39a18901f6..d5e594a1a5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.action; import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -45,7 +46,7 @@ import java.util.concurrent.Callable; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import static org.thingsboard.rule.engine.action.TbAlarmNode.*; +import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.*; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; import static org.thingsboard.server.common.data.alarm.AlarmStatus.*; @@ -53,7 +54,7 @@ import static org.thingsboard.server.common.data.alarm.AlarmStatus.*; @RunWith(MockitoJUnitRunner.class) public class TbAlarmNodeTest { - private TbAlarmNode node; + private TbAbstractAlarmNode node; @Mock private TbContext ctx; @@ -62,10 +63,6 @@ public class TbAlarmNodeTest { @Mock private AlarmService alarmService; - @Mock - private ScriptEngine createJs; - @Mock - private ScriptEngine clearJs; @Mock private ScriptEngine detailsJs; @@ -100,11 +97,10 @@ public class TbAlarmNodeTest { @Test public void newAlarmCanBeCreated() throws ScriptException, IOException { - initWithScript(); + initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); - when(createJs.executeFilter(msg)).thenReturn(true); when(detailsJs.executeJson(msg)).thenReturn(null); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); @@ -140,38 +136,15 @@ public class TbAlarmNodeTest { assertEquals(expectedAlarm, actualAlarm); - verify(executor, times(2)).executeAsync(any(Callable.class)); - } - - @Test - public void shouldCreateScriptThrowsException() throws ScriptException { - initWithScript(); - metaData.putValue("key", "value"); - TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); - - when(createJs.executeFilter(msg)).thenThrow(new NotImplementedException("message")); - - node.onMsg(ctx, msg); - - verifyError(msg, "message", NotImplementedException.class); - - - verify(ctx).createJsScriptEngine("CREATE", "isAlarm"); - verify(ctx).createJsScriptEngine("CLEAR", "isCleared"); - verify(ctx).createJsScriptEngine("DETAILS", "Details"); - verify(ctx).getJsExecutor(); - verify(ctx).getDbCallbackExecutor(); - - verifyNoMoreInteractions(ctx, alarmService, clearJs, detailsJs); + verify(executor, times(1)).executeAsync(any(Callable.class)); } @Test public void buildDetailsThrowsException() throws ScriptException, IOException { - initWithScript(); + initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); - when(createJs.executeFilter(msg)).thenReturn(true); when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message")); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); @@ -179,27 +152,24 @@ public class TbAlarmNodeTest { verifyError(msg, "message", NotImplementedException.class); - verify(ctx).createJsScriptEngine("CREATE", "isAlarm"); - verify(ctx).createJsScriptEngine("CLEAR", "isCleared"); verify(ctx).createJsScriptEngine("DETAILS", "Details"); - verify(ctx, times(2)).getJsExecutor(); + verify(ctx, times(1)).getJsExecutor(); verify(ctx).getAlarmService(); - verify(ctx, times(3)).getDbCallbackExecutor(); + verify(ctx, times(2)).getDbCallbackExecutor(); verify(ctx).getTenantId(); verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType"); - verifyNoMoreInteractions(ctx, alarmService, clearJs); + verifyNoMoreInteractions(ctx, alarmService); } @Test public void ifAlarmClearedCreateNew() throws ScriptException, IOException { - initWithScript(); + initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build(); - when(createJs.executeFilter(msg)).thenReturn(true); when(detailsJs.executeJson(msg)).thenReturn(null); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm)); @@ -236,20 +206,18 @@ public class TbAlarmNodeTest { assertEquals(expectedAlarm, actualAlarm); - verify(executor, times(2)).executeAsync(any(Callable.class)); + verify(executor, times(1)).executeAsync(any(Callable.class)); } @Test public void alarmCanBeUpdated() throws ScriptException, IOException { - initWithScript(); + initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); long oldEndDate = System.currentTimeMillis(); Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); - when(createJs.executeFilter(msg)).thenReturn(true); - when(clearJs.executeFilter(msg)).thenReturn(false); when(detailsJs.executeJson(msg)).thenReturn(null); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); @@ -287,23 +255,21 @@ public class TbAlarmNodeTest { assertEquals(expectedAlarm, actualAlarm); - verify(executor, times(2)).executeAsync(any(Callable.class)); + verify(executor, times(1)).executeAsync(any(Callable.class)); } @Test public void alarmCanBeCleared() throws ScriptException, IOException { - initWithScript(); + initWithClearAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); long oldEndDate = System.currentTimeMillis(); Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); - when(createJs.executeFilter(msg)).thenReturn(false); - when(clearJs.executeFilter(msg)).thenReturn(true); // when(detailsJs.executeJson(msg)).thenReturn(null); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getId()), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmService.clearAlarm(eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); // doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); node.onMsg(ctx, msg); @@ -338,20 +304,16 @@ public class TbAlarmNodeTest { assertEquals(expectedAlarm, actualAlarm); } - private void initWithScript() { + private void initWithCreateAlarmScript() { try { - TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration(); + TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); config.setPropagate(true); config.setSeverity(CRITICAL); config.setAlarmType("SomeType"); - config.setCreateConditionJs("CREATE"); - config.setClearConditionJs("CLEAR"); config.setAlarmDetailsBuildJs("DETAILS"); ObjectMapper mapper = new ObjectMapper(); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); - when(ctx.createJsScriptEngine("CREATE", "isAlarm")).thenReturn(createJs); - when(ctx.createJsScriptEngine("CLEAR", "isCleared")).thenReturn(clearJs); when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); when(ctx.getTenantId()).thenReturn(tenantId); @@ -361,7 +323,31 @@ public class TbAlarmNodeTest { mockJsExecutor(); - node = new TbAlarmNode(); + node = new TbCreateAlarmNode(); + node.init(ctx, nodeConfiguration); + } catch (TbNodeException ex) { + throw new IllegalStateException(ex); + } + } + + private void initWithClearAlarmScript() { + try { + TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration(); + config.setAlarmType("SomeType"); + config.setAlarmDetailsBuildJs("DETAILS"); + ObjectMapper mapper = new ObjectMapper(); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + + when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); + + when(ctx.getTenantId()).thenReturn(tenantId); + when(ctx.getJsExecutor()).thenReturn(executor); + when(ctx.getAlarmService()).thenReturn(alarmService); + when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + + mockJsExecutor(); + + node = new TbClearAlarmNode(); node.init(ctx, nodeConfiguration); } catch (TbNodeException ex) { throw new IllegalStateException(ex); diff --git a/ui/src/app/alarm/alarm-table.directive.js b/ui/src/app/alarm/alarm-table.directive.js index 03470c647f..775088cb42 100644 --- a/ui/src/app/alarm/alarm-table.directive.js +++ b/ui/src/app/alarm/alarm-table.directive.js @@ -42,7 +42,7 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope history: { timewindowMs: 24 * 60 * 60 * 1000 // 1 day } - } + }; scope.topIndex = 0; @@ -98,6 +98,8 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope } }; + scope.reload = reload; + scope.$watch("entityId", function(newVal, prevVal) { if (newVal && !angular.equals(newVal, prevVal)) { resetFilter(); diff --git a/ui/src/app/alarm/alarm-table.tpl.html b/ui/src/app/alarm/alarm-table.tpl.html index b9e466ffea..658362e51b 100644 --- a/ui/src/app/alarm/alarm-table.tpl.html +++ b/ui/src/app/alarm/alarm-table.tpl.html @@ -26,6 +26,13 @@ + + refresh + + {{ 'action.refresh' | translate }} + +
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 89399b03a8..141ac673bb 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1242,7 +1242,8 @@ export default angular.module('thingsboard.locale', []) "metadata": "Metadata", "metadata-required": "Metadata entries can't be empty.", "output": "Output", - "test": "Test" + "test": "Test", + "help": "Help" }, "rule-plugin": { "management": "Rules and plugins management" diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js index 0b63335c17..130d5aeb5a 100644 --- a/ui/src/app/rulechain/rulechain.controller.js +++ b/ui/src/app/rulechain/rulechain.controller.js @@ -489,7 +489,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time function displayNodeDescriptionTooltip(event, node) { displayTooltip(event, '
' + - '
' + + '
' + '
' + node.component.name + '
' + '
' + node.component.configurationDescriptor.nodeDefinition.description + '
' + '
' + node.component.configurationDescriptor.nodeDefinition.details + '
' + diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss index 1d6cb972fb..a4a708cbc3 100644 --- a/ui/src/app/rulechain/rulechain.scss +++ b/ui/src/app/rulechain/rulechain.scss @@ -431,10 +431,17 @@ } } +.tb-rule-node-tooltip, .tb-rule-node-help { + color: #333; +} + .tb-rule-node-tooltip { font-size: 14px; width: 300px; - color: #333; +} + +.tb-rule-node-help { + font-size: 16px; } .tb-rule-node-error-tooltip { @@ -442,8 +449,8 @@ color: #ea0d0d; } -.tb-rule-node-tooltip, .tb-rule-node-error-tooltip { - #tooltip-content { +.tb-rule-node-tooltip, .tb-rule-node-error-tooltip, .tb-rule-node-help { + #tb-node-content { .tb-node-title { font-weight: 600; } diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html index 11a0751a9d..4ba967d687 100644 --- a/ui/src/app/rulechain/rulechain.tpl.html +++ b/ui/src/app/rulechain/rulechain.tpl.html @@ -178,6 +178,17 @@ default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}"> + +
+
+
{{vm.editingRuleNode.component.name}}
+
 
+
{{vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
+
 
+
+
+
+
Date: Fri, 11 May 2018 20:04:02 +0300 Subject: [PATCH 2/5] Update post telemetry message body format. Add relationType to rule nodes msg debug. --- .../server/actors/ActorSystemContext.java | 19 +++++----- .../device/DeviceActorMessageProcessor.java | 15 ++++---- .../actors/ruleChain/DefaultTbContext.java | 4 +-- .../RuleChainActorMessageProcessor.java | 36 +++++++++---------- .../ruleChain/RuleChainToRuleChainMsg.java | 1 + .../ruleChain/RuleChainToRuleNodeMsg.java | 1 + .../RuleNodeActorMessageProcessor.java | 4 +-- .../transport/adaptor/JsonConverter.java | 9 ++++- .../rule/engine/filter/TbJsFilterNode.java | 2 +- .../filter/TbJsSwitchNodeConfiguration.java | 2 +- .../engine/filter/TbMsgTypeFilterNode.java | 2 +- .../rule/engine/mqtt/TbMqttNode.java | 3 +- .../rule/engine/rabbitmq/TbRabbitMqNode.java | 2 +- .../engine/telemetry/TbMsgTimeseriesNode.java | 16 +++++++-- .../event-header-debug-rulenode.tpl.html | 2 +- .../event/event-row-debug-rulenode.tpl.html | 2 +- ui/src/app/locale/locale.constant.js | 1 + 17 files changed, 70 insertions(+), 51 deletions(-) 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 54737f84d5..665d2d9489 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -312,23 +312,23 @@ public class ActorSystemContext { return discoveryService.getCurrentServer().getServerAddress().toString(); } - public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { - persistDebug(tenantId, entityId, "IN", tbMsg, null); + public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) { + persistDebug(tenantId, entityId, "IN", tbMsg, relationType, null); } - public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) { - persistDebug(tenantId, entityId, "IN", tbMsg, error); + public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) { + persistDebug(tenantId, entityId, "IN", tbMsg, relationType, error); } - public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) { - persistDebug(tenantId, entityId, "OUT", tbMsg, error); + public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) { + persistDebug(tenantId, entityId, "OUT", tbMsg, relationType, error); } - public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { - persistDebug(tenantId, entityId, "OUT", tbMsg, null); + public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) { + persistDebug(tenantId, entityId, "OUT", tbMsg, relationType, null); } - private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) { + private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, String relationType, Throwable error) { try { Event event = new Event(); event.setTenantId(tenantId); @@ -345,6 +345,7 @@ public class ActorSystemContext { .put("msgId", tbMsg.getId().toString()) .put("msgType", tbMsg.getType()) .put("dataType", tbMsg.getDataType().name()) + .put("relationType", relationType) .put("data", tbMsg.getData()) .put("metadata", metadata); diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index dd270e2757..f5f5848473 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -355,16 +355,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso for (Map.Entry> entry : tsData.entrySet()) { JsonObject json = new JsonObject(); - json.addProperty("ts", entry.getKey()); - JsonObject values = new JsonObject(); for (KvEntry kv : entry.getValue()) { - kv.getBooleanValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); - kv.getLongValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); - kv.getDoubleValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); - kv.getStrValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); + kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v)); + kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v)); + kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v)); + kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v)); } - json.add("values", values); - TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, defaultMetaData.copy(), TbMsgDataType.JSON, gson.toJson(json), null, null, 0L); + TbMsgMetaData metaData = defaultMetaData.copy(); + metaData.putValue("ts", entry.getKey()+""); + TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, metaData, TbMsgDataType.JSON, gson.toJson(json), null, null, 0L); pushToRuleEngineWithTimeout(context, tbMsg, msgData); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 039fdf6127..2e8955bf1f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -69,7 +69,7 @@ class DefaultTbContext implements TbContext { @Override public void tellNext(TbMsg msg, String relationType, Throwable th) { if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, th); + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th); } nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationType, msg), nodeCtx.getSelfActor()); } @@ -102,7 +102,7 @@ class DefaultTbContext implements TbContext { @Override public void tellError(TbMsg msg, Throwable th) { if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, th); + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, "", th); } nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor()); } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 9c60327b8b..d069cb0eae 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -96,12 +96,12 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor ruleNodeList) { for (RuleNode ruleNode : ruleNodeList) { for (TbMsg tbMsg : queue.findUnprocessed(ruleNode.getId().getId(), 0L)) { - pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg); + pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg, ""); } } if (firstNode != null) { for (TbMsg tbMsg : queue.findUnprocessed(entityId.getId(), 0L)) { - pushMsgToNode(firstNode, tbMsg); + pushMsgToNode(firstNode, tbMsg, ""); } } } @@ -183,13 +183,13 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor pushMsgToNode(firstNode, msg)); + putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> pushMsgToNode(firstNode, msg, "")); } void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg envelope) { checkActive(); putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> { - pushMsgToNode(firstNode, msg); + pushMsgToNode(firstNode, msg, ""); envelope.getCallbackRef().tell(new RuleEngineQueuePutAckMsg(msg.getId()), self); }); } @@ -197,9 +197,9 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor pushMsgToNode(firstNode, msg)); + putToQueue(enrichWithRuleChainId(envelope.getMsg()), msg -> pushMsgToNode(firstNode, msg, envelope.getFromRelationType())); } else { - pushMsgToNode(firstNode, envelope.getMsg()); + pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType()); } } @@ -218,17 +218,17 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor pushMsgToNode(targetNodeCtx, queuedMsg)); + putToQueue(copy, queuedMsg -> pushMsgToNode(targetNodeCtx, queuedMsg, fromRelationType)); } - private void pushToTarget(TbMsg msg, EntityId target) { + private void pushToTarget(TbMsg msg, EntityId target, String fromRelationType) { switch (target.getEntityType()) { case RULE_NODE: - pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg); + pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg, fromRelationType); break; case RULE_CHAIN: - parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, msg, false), self); + parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, msg, fromRelationType, false), self); break; } } - private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg) { + private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) { if (nodeCtx != null) { - nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg), self); + nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg, fromRelationType), self); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java index 2b2623bf09..086164680b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java @@ -31,6 +31,7 @@ public final class RuleChainToRuleChainMsg implements TbActorMsg { private final RuleChainId target; private final RuleChainId source; private final TbMsg msg; + private final String fromRelationType; private final boolean enqueue; @Override diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java index e7d866c1eb..abcddc91fc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java @@ -29,6 +29,7 @@ final class RuleChainToRuleNodeMsg implements TbActorMsg { private final TbContext ctx; private final TbMsg msg; + private final String fromRelationType; @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java index ea857dbbb4..f23ba6b9e5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java @@ -93,7 +93,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor jsEngine.executeFilter(msg)), - filterResult -> ctx.tellNext(msg, Boolean.toString(filterResult)), + filterResult -> ctx.tellNext(msg, filterResult.booleanValue() ? "True" : "False"), t -> ctx.tellError(msg, t)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java index 4c5808b236..671132d8f9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java @@ -31,7 +31,7 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration { if (future.isSuccess()) { - TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData()); - ctx.tellNext(next, TbRelationTypes.SUCCESS); + ctx.tellNext(msg, TbRelationTypes.SUCCESS); } else { TbMsg next = processException(ctx, msg, future.cause()); ctx.tellNext(next, TbRelationTypes.FAILURE, future.cause()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 69e7c1de3f..993b172ddf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -106,7 +106,7 @@ public class TbRabbitMqNode implements TbNode { routingKey, properties, msg.getData().getBytes(UTF8)); - return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData()); + return msg; } private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 114ca27390..d91d5d41ad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -63,12 +63,22 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellError(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - + long ts = -1; + String tsStr = msg.getMetaData().getValue("ts"); + if (!StringUtils.isEmpty(tsStr)) { + try { + ts = Long.parseLong(tsStr); + } catch (NumberFormatException e) {} + } + if (ts == -1) { + ctx.tellError(msg, new IllegalArgumentException("Msg metadata doesn't contain valid ts value: " + msg.getMetaData())); + return; + } String src = msg.getData(); - TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src)); + TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); Map> tsKvMap = telemetryUploadRequest.getData(); if (tsKvMap == null) { - ctx.tellError(msg, new IllegalArgumentException("Msg body us empty: " + src)); + ctx.tellError(msg, new IllegalArgumentException("Msg body is empty: " + src)); return; } List tsKvEntryList = new ArrayList<>(); diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html index 34f4513655..bbf6ae0cef 100644 --- a/ui/src/app/event/event-header-debug-rulenode.tpl.html +++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html @@ -21,7 +21,7 @@
event.entity
event.message-id
event.message-type
-
event.data-type
+
event.relation-type
event.data
event.metadata
event.error
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html index bb832b15d8..857cd8cbe6 100644 --- a/ui/src/app/event/event-row-debug-rulenode.tpl.html +++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html @@ -21,7 +21,7 @@
{{event.body.entityName}}
{{event.body.msgId}}
{{event.body.msgType}}
-
{{event.body.dataType}}
+
{{event.body.relationType}}
Date: Mon, 14 May 2018 11:20:24 +0300 Subject: [PATCH 3/5] Add Netty MQTT Client module. --- netty-mqtt/.gitignore | 7 + netty-mqtt/pom.xml | 99 ++++ .../mqtt/ChannelClosedException.java | 43 ++ .../thingsboard/mqtt/MqttChannelHandler.java | 269 ++++++++++ .../java/org/thingsboard/mqtt/MqttClient.java | 205 ++++++++ .../thingsboard/mqtt/MqttClientCallback.java | 29 ++ .../thingsboard/mqtt/MqttClientConfig.java | 149 ++++++ .../org/thingsboard/mqtt/MqttClientImpl.java | 484 ++++++++++++++++++ .../thingsboard/mqtt/MqttConnectResult.java | 45 ++ .../org/thingsboard/mqtt/MqttHandler.java | 23 + .../mqtt/MqttIncomingQos2Publish.java | 48 ++ .../org/thingsboard/mqtt/MqttLastWill.java | 154 ++++++ .../thingsboard/mqtt/MqttPendingPublish.java | 101 ++++ .../mqtt/MqttPendingSubscribtion.java | 102 ++++ .../mqtt/MqttPendingUnsubscribtion.java | 55 ++ .../org/thingsboard/mqtt/MqttPingHandler.java | 98 ++++ .../thingsboard/mqtt/MqttSubscribtion.java | 84 +++ .../mqtt/RetransmissionHandler.java | 66 +++ pom.xml | 17 +- rule-engine/rule-engine-components/pom.xml | 8 +- .../rule/engine/mqtt/TbMqttNode.java | 6 +- .../credentials/AnonymousCredentials.java | 2 +- .../mqtt/credentials/BasicCredentials.java | 2 +- .../credentials/CertPemClientCredentials.java | 2 +- .../credentials/MqttClientCredentials.java | 2 +- 25 files changed, 2083 insertions(+), 17 deletions(-) create mode 100644 netty-mqtt/.gitignore create mode 100644 netty-mqtt/pom.xml create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java diff --git a/netty-mqtt/.gitignore b/netty-mqtt/.gitignore new file mode 100644 index 0000000000..4d2302b226 --- /dev/null +++ b/netty-mqtt/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.ipr +*.iws +*.ids +*.iml +logs +target diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml new file mode 100644 index 0000000000..4e9c84ffd5 --- /dev/null +++ b/netty-mqtt/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.thingsboard + 1.4.1-SNAPSHOT + thingsboard + + org.thingsboard + netty-mqtt + 1.4.1-SNAPSHOT + jar + + Netty MQTT Client + https://thingsboard.io + + + UTF-8 + ${basedir}/.. + + + + + io.netty + netty-codec-mqtt + + + io.netty + netty-handler + + + com.google.code.findbugs + jsr305 + 3.0.1 + true + + + com.google.guava + guava + + + + + + jk-5-maven + jk-5's maven server + sftp://10.2.1.2/opt/maven + + + + + + + org.apache.maven.wagon + wagon-ssh + 2.6 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + + + + + + + \ No newline at end of file diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java new file mode 100644 index 0000000000..a76b082ba9 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +/** + * Created by Valerii Sosliuk on 12/26/2017. + */ +public class ChannelClosedException extends RuntimeException { + + private static final long serialVersionUID = 6266638352424706909L; + + public ChannelClosedException() { + } + + public ChannelClosedException(String message) { + super(message); + } + + public ChannelClosedException(String message, Throwable cause) { + super(message, cause); + } + + public ChannelClosedException(Throwable cause) { + super(cause); + } + + public ChannelClosedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java new file mode 100644 index 0000000000..ef5e7a531d --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java @@ -0,0 +1,269 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import com.google.common.collect.ImmutableSet; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.mqtt.*; +import io.netty.util.CharsetUtil; +import io.netty.util.concurrent.Promise; + +final class MqttChannelHandler extends SimpleChannelInboundHandler { + + private final MqttClientImpl client; + private final Promise connectFuture; + + MqttChannelHandler(MqttClientImpl client, Promise connectFuture) { + this.client = client; + this.connectFuture = connectFuture; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception { + switch (msg.fixedHeader().messageType()) { + case CONNACK: + handleConack(ctx.channel(), (MqttConnAckMessage) msg); + break; + case SUBACK: + handleSubAck((MqttSubAckMessage) msg); + break; + case PUBLISH: + handlePublish(ctx.channel(), (MqttPublishMessage) msg); + break; + case UNSUBACK: + handleUnsuback((MqttUnsubAckMessage) msg); + break; + case PUBACK: + handlePuback((MqttPubAckMessage) msg); + break; + case PUBREC: + handlePubrec(ctx.channel(), msg); + break; + case PUBREL: + handlePubrel(ctx.channel(), msg); + break; + case PUBCOMP: + handlePubcomp(msg); + break; + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader( + this.client.getClientConfig().getProtocolVersion().protocolName(), // Protocol Name + this.client.getClientConfig().getProtocolVersion().protocolLevel(), // Protocol Level + this.client.getClientConfig().getUsername() != null, // Has Username + this.client.getClientConfig().getPassword() != null, // Has Password + this.client.getClientConfig().getLastWill() != null // Will Retain + && this.client.getClientConfig().getLastWill().isRetain(), + this.client.getClientConfig().getLastWill() != null // Will QOS + ? this.client.getClientConfig().getLastWill().getQos().value() + : 0, + this.client.getClientConfig().getLastWill() != null, // Has Will + this.client.getClientConfig().isCleanSession(), // Clean Session + this.client.getClientConfig().getTimeoutSeconds() // Timeout + ); + MqttConnectPayload payload = new MqttConnectPayload( + this.client.getClientConfig().getClientId(), + this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getTopic() : null, + this.client.getClientConfig().getLastWill() != null ? this.client.getClientConfig().getLastWill().getMessage().getBytes(CharsetUtil.UTF_8) : null, + this.client.getClientConfig().getUsername(), + this.client.getClientConfig().getPassword() != null ? this.client.getClientConfig().getPassword().getBytes(CharsetUtil.UTF_8) : null + ); + ctx.channel().writeAndFlush(new MqttConnectMessage(fixedHeader, variableHeader, payload)); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + } + + private void invokeHandlersForIncomingPublish(MqttPublishMessage message) { + for (MqttSubscribtion subscribtion : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { + if (subscribtion.matches(message.variableHeader().topicName())) { + if (subscribtion.isOnce() && subscribtion.isCalled()) { + continue; + } + message.payload().markReaderIndex(); + subscribtion.setCalled(true); + subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); + if (subscribtion.isOnce()) { + this.client.off(subscribtion.getTopic(), subscribtion.getHandler()); + } + message.payload().resetReaderIndex(); + } + } + /*Set subscribtions = ImmutableSet.copyOf(this.client.getSubscriptions().get(message.variableHeader().topicName())); + for (MqttSubscribtion subscribtion : subscribtions) { + if(subscribtion.isOnce() && subscribtion.isCalled()){ + continue; + } + message.payload().markReaderIndex(); + subscribtion.setCalled(true); + subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); + if(subscribtion.isOnce()){ + this.client.off(subscribtion.getTopic(), subscribtion.getHandler()); + } + message.payload().resetReaderIndex(); + }*/ + message.payload().release(); + } + + private void handleConack(Channel channel, MqttConnAckMessage message) { + switch (message.variableHeader().connectReturnCode()) { + case CONNECTION_ACCEPTED: + this.connectFuture.setSuccess(new MqttConnectResult(true, MqttConnectReturnCode.CONNECTION_ACCEPTED, channel.closeFuture())); + + this.client.getPendingSubscribtions().entrySet().stream().filter((e) -> !e.getValue().isSent()).forEach((e) -> { + channel.write(e.getValue().getSubscribeMessage()); + e.getValue().setSent(true); + }); + + this.client.getPendingPublishes().forEach((id, publish) -> { + if (publish.isSent()) return; + channel.write(publish.getMessage()); + publish.setSent(true); + if (publish.getQos() == MqttQoS.AT_MOST_ONCE) { + publish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 + this.client.getPendingPublishes().remove(publish.getMessageId()); + } + }); + channel.flush(); + break; + + case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: + case CONNECTION_REFUSED_IDENTIFIER_REJECTED: + case CONNECTION_REFUSED_NOT_AUTHORIZED: + case CONNECTION_REFUSED_SERVER_UNAVAILABLE: + case CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION: + this.connectFuture.setSuccess(new MqttConnectResult(false, message.variableHeader().connectReturnCode(), channel.closeFuture())); + channel.close(); + // Don't start reconnect logic here + break; + } + } + + private void handleSubAck(MqttSubAckMessage message) { + MqttPendingSubscribtion pendingSubscription = this.client.getPendingSubscribtions().remove(message.variableHeader().messageId()); + if (pendingSubscription == null) { + return; + } + pendingSubscription.onSubackReceived(); + for (MqttPendingSubscribtion.MqttPendingHandler handler : pendingSubscription.getHandlers()) { + MqttSubscribtion subscribtion = new MqttSubscribtion(pendingSubscription.getTopic(), handler.getHandler(), handler.isOnce()); + this.client.getSubscriptions().put(pendingSubscription.getTopic(), subscribtion); + this.client.getHandlerToSubscribtion().put(handler.getHandler(), subscribtion); + } + this.client.getPendingSubscribeTopics().remove(pendingSubscription.getTopic()); + + this.client.getServerSubscribtions().add(pendingSubscription.getTopic()); + + if (!pendingSubscription.getFuture().isDone()) { + pendingSubscription.getFuture().setSuccess(null); + } + } + + private void handlePublish(Channel channel, MqttPublishMessage message) { + switch (message.fixedHeader().qosLevel()) { + case AT_MOST_ONCE: + invokeHandlersForIncomingPublish(message); + break; + + case AT_LEAST_ONCE: + invokeHandlersForIncomingPublish(message); + if (message.variableHeader().messageId() != -1) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId()); + channel.writeAndFlush(new MqttPubAckMessage(fixedHeader, variableHeader)); + } + break; + + case EXACTLY_ONCE: + if (message.variableHeader().messageId() != -1) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(message.variableHeader().messageId()); + MqttMessage pubrecMessage = new MqttMessage(fixedHeader, variableHeader); + + MqttIncomingQos2Publish incomingQos2Publish = new MqttIncomingQos2Publish(message, pubrecMessage); + this.client.getQos2PendingIncomingPublishes().put(message.variableHeader().messageId(), incomingQos2Publish); + message.payload().retain(); + incomingQos2Publish.startPubrecRetransmitTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket); + + channel.writeAndFlush(pubrecMessage); + } + break; + } + } + + private void handleUnsuback(MqttUnsubAckMessage message) { + MqttPendingUnsubscribtion unsubscribtion = this.client.getPendingServerUnsubscribes().get(message.variableHeader().messageId()); + if (unsubscribtion == null) { + return; + } + unsubscribtion.onUnsubackReceived(); + this.client.getServerSubscribtions().remove(unsubscribtion.getTopic()); + unsubscribtion.getFuture().setSuccess(null); + this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId()); + } + + private void handlePuback(MqttPubAckMessage message) { + MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId()); + pendingPublish.getFuture().setSuccess(null); + pendingPublish.onPubackReceived(); + this.client.getPendingPublishes().remove(message.variableHeader().messageId()); + pendingPublish.getPayload().release(); + } + + private void handlePubrec(Channel channel, MqttMessage message) { + MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); + pendingPublish.onPubackReceived(); + + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBREL, false, MqttQoS.AT_LEAST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); + MqttMessage pubrelMessage = new MqttMessage(fixedHeader, variableHeader); + channel.writeAndFlush(pubrelMessage); + + pendingPublish.setPubrelMessage(pubrelMessage); + pendingPublish.startPubrelRetransmissionTimer(this.client.getEventLoop().next(), this.client::sendAndFlushPacket); + } + + private void handlePubrel(Channel channel, MqttMessage message) { + if (this.client.getQos2PendingIncomingPublishes().containsKey(((MqttMessageIdVariableHeader) message.variableHeader()).messageId())) { + MqttIncomingQos2Publish incomingQos2Publish = this.client.getQos2PendingIncomingPublishes().get(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); + this.invokeHandlersForIncomingPublish(incomingQos2Publish.getIncomingPublish()); + incomingQos2Publish.onPubrelReceived(); + this.client.getQos2PendingIncomingPublishes().remove(incomingQos2Publish.getIncomingPublish().variableHeader().messageId()); + } + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(((MqttMessageIdVariableHeader) message.variableHeader()).messageId()); + channel.writeAndFlush(new MqttMessage(fixedHeader, variableHeader)); + } + + private void handlePubcomp(MqttMessage message) { + MqttMessageIdVariableHeader variableHeader = (MqttMessageIdVariableHeader) message.variableHeader(); + MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(variableHeader.messageId()); + pendingPublish.getFuture().setSuccess(null); + this.client.getPendingPublishes().remove(variableHeader.messageId()); + pendingPublish.getPayload().release(); + pendingPublish.onPubcompReceived(); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java new file mode 100644 index 0000000000..6563525783 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java @@ -0,0 +1,205 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.util.concurrent.Future; + +public interface MqttClient { + + /** + * Connect to the specified hostname/ip. By default uses port 1883. + * If you want to change the port number, see {@link #connect(String, int)} + * + * @param host The ip address or host to connect to + * @return A future which will be completed when the connection is opened and we received an CONNACK + */ + Future connect(String host); + + /** + * Connect to the specified hostname/ip using the specified port + * + * @param host The ip address or host to connect to + * @param port The tcp port to connect to + * @return A future which will be completed when the connection is opened and we received an CONNACK + */ + Future connect(String host, int port); + + /** + * + * @return boolean value indicating if channel is active + */ + boolean isConnected(); + + /** + * Attempt reconnect to the host that was attempted with {@link #connect(String, int)} method before + * + * @return A future which will be completed when the connection is opened and we received an CONNACK + * @throws IllegalStateException if no previous {@link #connect(String, int)} calls were attempted + */ + Future reconnect(); + + /** + * Retrieve the netty {@link EventLoopGroup} we are using + * @return The netty {@link EventLoopGroup} we use for the connection + */ + EventLoopGroup getEventLoop(); + + /** + * By default we use the netty {@link NioEventLoopGroup}. + * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)} + * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)} + * + * @param eventLoop The new eventloop to use + */ + void setEventLoop(EventLoopGroup eventLoop); + + /** + * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @return A future which will be completed when the server acknowledges our subscribe request + */ + Future on(String topic, MqttHandler handler); + + /** + * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @param qos The qos to request to the server + * @return A future which will be completed when the server acknowledges our subscribe request + */ + Future on(String topic, MqttHandler handler, MqttQoS qos); + + /** + * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @return A future which will be completed when the server acknowledges our subscribe request + */ + Future once(String topic, MqttHandler handler); + + /** + * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @param qos The qos to request to the server + * @return A future which will be completed when the server acknowledges our subscribe request + */ + Future once(String topic, MqttHandler handler, MqttQoS qos); + + /** + * Remove the subscribtion for the given topic and handler + * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} + * + * @param topic The topic to unsubscribe for + * @param handler The handler to unsubscribe + * @return A future which will be completed when the server acknowledges our unsubscribe request + */ + Future off(String topic, MqttHandler handler); + + /** + * Remove all subscribtions for the given topic. + * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} + * + * @param topic The topic to unsubscribe for + * @return A future which will be completed when the server acknowledges our unsubscribe request + */ + Future off(String topic); + + /** + * Publish a message to the given payload + * @param topic The topic to publish to + * @param payload The payload to send + * @return A future which will be completed when the message is sent out of the MqttClient + */ + Future publish(String topic, ByteBuf payload); + + /** + * Publish a message to the given payload, using the given qos + * @param topic The topic to publish to + * @param payload The payload to send + * @param qos The qos to use while publishing + * @return A future which will be completed when the message is delivered to the server + */ + Future publish(String topic, ByteBuf payload, MqttQoS qos); + + /** + * Publish a message to the given payload, using optional retain + * @param topic The topic to publish to + * @param payload The payload to send + * @param retain true if you want to retain the message on the server, false otherwise + * @return A future which will be completed when the message is sent out of the MqttClient + */ + Future publish(String topic, ByteBuf payload, boolean retain); + + /** + * Publish a message to the given payload, using the given qos and optional retain + * @param topic The topic to publish to + * @param payload The payload to send + * @param qos The qos to use while publishing + * @param retain true if you want to retain the message on the server, false otherwise + * @return A future which will be completed when the message is delivered to the server + */ + Future publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain); + + /** + * Retrieve the MqttClient configuration + * @return The {@link MqttClientConfig} instance we use + */ + MqttClientConfig getClientConfig(); + + /** + * Construct the MqttClientImpl with default config + */ + static MqttClient create(){ + return new MqttClientImpl(); + } + + /** + * Construct the MqttClientImpl with additional config. + * This config can also be changed using the {@link #getClientConfig()} function + * + * @param config The config object to use while looking for settings + */ + static MqttClient create(MqttClientConfig config){ + return new MqttClientImpl(config); + } + + + /** + * Send disconnect and close channel + * + */ + void disconnect(); + + /** + * Sets the {@see #MqttClientCallback} object for this MqttClient + * @param callback The callback to be set + */ + void setCallback(MqttClientCallback callback); + +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java new file mode 100644 index 0000000000..d7f0a08409 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +/** + * Created by Valerii Sosliuk on 12/30/2017. + */ +public interface MqttClientCallback { + + /** + * This method is called when the connection to the server is lost. + * + * @param cause the reason behind the loss of connection. + */ + public void connectionLost(Throwable cause); +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java new file mode 100644 index 0000000000..a59d83b14b --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java @@ -0,0 +1,149 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.Channel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.mqtt.MqttVersion; +import io.netty.handler.ssl.SslContext; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Random; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class MqttClientConfig { + + private final SslContext sslContext; + private final String randomClientId; + + private String clientId; + private int timeoutSeconds = 60; + private MqttVersion protocolVersion = MqttVersion.MQTT_3_1; + @Nullable private String username = null; + @Nullable private String password = null; + private boolean cleanSession = true; + @Nullable private MqttLastWill lastWill; + private Class channelClass = NioSocketChannel.class; + + private boolean reconnect = true; + + public MqttClientConfig() { + this(null); + } + + public MqttClientConfig(SslContext sslContext) { + this.sslContext = sslContext; + Random random = new Random(); + String id = "netty-mqtt/"; + String[] options = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(""); + for(int i = 0; i < 8; i++){ + id += options[random.nextInt(options.length)]; + } + this.clientId = id; + this.randomClientId = id; + } + + @Nonnull + public String getClientId() { + return clientId; + } + + public void setClientId(@Nullable String clientId) { + if(clientId == null){ + this.clientId = randomClientId; + }else{ + this.clientId = clientId; + } + } + + public int getTimeoutSeconds() { + return timeoutSeconds; + } + + public void setTimeoutSeconds(int timeoutSeconds) { + if(timeoutSeconds != -1 && timeoutSeconds <= 0){ + throw new IllegalArgumentException("timeoutSeconds must be > 0 or -1"); + } + this.timeoutSeconds = timeoutSeconds; + } + + public MqttVersion getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(MqttVersion protocolVersion) { + if(protocolVersion == null){ + throw new NullPointerException("protocolVersion"); + } + this.protocolVersion = protocolVersion; + } + + @Nullable + public String getUsername() { + return username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + @Nullable + public String getPassword() { + return password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + public boolean isCleanSession() { + return cleanSession; + } + + public void setCleanSession(boolean cleanSession) { + this.cleanSession = cleanSession; + } + + @Nullable + public MqttLastWill getLastWill() { + return lastWill; + } + + public void setLastWill(@Nullable MqttLastWill lastWill) { + this.lastWill = lastWill; + } + + public Class getChannelClass() { + return channelClass; + } + + public void setChannelClass(Class channelClass) { + this.channelClass = channelClass; + } + + public SslContext getSslContext() { + return sslContext; + } + + public boolean isReconnect() { + return reconnect; + } + + public void setReconnect(boolean reconnect) { + this.reconnect = reconnect; + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java new file mode 100644 index 0000000000..391410525f --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -0,0 +1,484 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.mqtt.*; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +final class MqttClientImpl implements MqttClient { + + private final Set serverSubscribtions = new HashSet<>(); + private final IntObjectHashMap pendingServerUnsubscribes = new IntObjectHashMap<>(); + private final IntObjectHashMap qos2PendingIncomingPublishes = new IntObjectHashMap<>(); + private final IntObjectHashMap pendingPublishes = new IntObjectHashMap<>(); + private final HashMultimap subscriptions = HashMultimap.create(); + private final IntObjectHashMap pendingSubscribtions = new IntObjectHashMap<>(); + private final Set pendingSubscribeTopics = new HashSet<>(); + private final HashMultimap handlerToSubscribtion = HashMultimap.create(); + private final AtomicInteger nextMessageId = new AtomicInteger(1); + + private final MqttClientConfig clientConfig; + + private EventLoopGroup eventLoop; + + private Channel channel; + + private boolean disconnected = false; + private String host; + private int port; + private MqttClientCallback callback; + + + /** + * Construct the MqttClientImpl with default config + */ + public MqttClientImpl() { + this.clientConfig = new MqttClientConfig(); + } + + /** + * Construct the MqttClientImpl with additional config. + * This config can also be changed using the {@link #getClientConfig()} function + * + * @param clientConfig The config object to use while looking for settings + */ + public MqttClientImpl(MqttClientConfig clientConfig) { + this.clientConfig = clientConfig; + } + + /** + * Connect to the specified hostname/ip. By default uses port 1883. + * If you want to change the port number, see {@link #connect(String, int)} + * + * @param host The ip address or host to connect to + * @return A future which will be completed when the connection is opened and we received an CONNACK + */ + @Override + public Future connect(String host) { + return connect(host, 1883); + } + + /** + * Connect to the specified hostname/ip using the specified port + * + * @param host The ip address or host to connect to + * @param port The tcp port to connect to + * @return A future which will be completed when the connection is opened and we received an CONNACK + */ + @Override + public Future connect(String host, int port) { + if (this.eventLoop == null) { + this.eventLoop = new NioEventLoopGroup(); + } + this.host = host; + this.port = port; + + Promise connectFuture = new DefaultPromise<>(this.eventLoop.next()); + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(this.eventLoop); + bootstrap.channel(clientConfig.getChannelClass()); + bootstrap.remoteAddress(host, port); + bootstrap.handler(new MqttChannelInitializer(connectFuture, host, port, clientConfig.getSslContext())); + ChannelFuture future = bootstrap.connect(); + future.addListener((ChannelFutureListener) f -> { + if (f.isSuccess()) { + MqttClientImpl.this.channel = f.channel(); + } else if (clientConfig.isReconnect() && !disconnected) { + eventLoop.schedule((Runnable) () -> connect(host, port), 1L, TimeUnit.SECONDS); + } + }); + return connectFuture; + } + + @Override + public boolean isConnected() { + if (!disconnected) { + return channel == null ? false : channel.isActive(); + }; + return false; + } + + @Override + public Future reconnect() { + if (host == null) { + throw new IllegalStateException("Cannot reconnect. Call connect() first"); + } + return connect(host, port); + } + + /** + * Retrieve the netty {@link EventLoopGroup} we are using + * + * @return The netty {@link EventLoopGroup} we use for the connection + */ + @Override + public EventLoopGroup getEventLoop() { + return eventLoop; + } + + /** + * By default we use the netty {@link NioEventLoopGroup}. + * If you change the EventLoopGroup to another type, make sure to change the {@link Channel} class using {@link MqttClientConfig#setChannelClass(Class)} + * If you want to force the MqttClient to use another {@link EventLoopGroup}, call this function before calling {@link #connect(String, int)} + * + * @param eventLoop The new eventloop to use + */ + @Override + public void setEventLoop(EventLoopGroup eventLoop) { + this.eventLoop = eventLoop; + } + + /** + * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @return A future which will be completed when the server acknowledges our subscribe request + */ + @Override + public Future on(String topic, MqttHandler handler) { + return on(topic, handler, MqttQoS.AT_MOST_ONCE); + } + + /** + * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @param qos The qos to request to the server + * @return A future which will be completed when the server acknowledges our subscribe request + */ + @Override + public Future on(String topic, MqttHandler handler, MqttQoS qos) { + return createSubscribtion(topic, handler, false, qos); + } + + /** + * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @return A future which will be completed when the server acknowledges our subscribe request + */ + @Override + public Future once(String topic, MqttHandler handler) { + return once(topic, handler, MqttQoS.AT_MOST_ONCE); + } + + /** + * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler + * This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed + * + * @param topic The topic filter to subscribe to + * @param handler The handler to invoke when we receive a message + * @param qos The qos to request to the server + * @return A future which will be completed when the server acknowledges our subscribe request + */ + @Override + public Future once(String topic, MqttHandler handler, MqttQoS qos) { + return createSubscribtion(topic, handler, true, qos); + } + + /** + * Remove the subscribtion for the given topic and handler + * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} + * + * @param topic The topic to unsubscribe for + * @param handler The handler to unsubscribe + * @return A future which will be completed when the server acknowledges our unsubscribe request + */ + @Override + public Future off(String topic, MqttHandler handler) { + Promise future = new DefaultPromise<>(this.eventLoop.next()); + for (MqttSubscribtion subscribtion : this.handlerToSubscribtion.get(handler)) { + this.subscriptions.remove(topic, subscribtion); + } + this.handlerToSubscribtion.removeAll(handler); + this.checkSubscribtions(topic, future); + return future; + } + + /** + * Remove all subscribtions for the given topic. + * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} + * + * @param topic The topic to unsubscribe for + * @return A future which will be completed when the server acknowledges our unsubscribe request + */ + @Override + public Future off(String topic) { + Promise future = new DefaultPromise<>(this.eventLoop.next()); + ImmutableSet subscribtions = ImmutableSet.copyOf(this.subscriptions.get(topic)); + for (MqttSubscribtion subscribtion : subscribtions) { + for (MqttSubscribtion handSub : this.handlerToSubscribtion.get(subscribtion.getHandler())) { + this.subscriptions.remove(topic, handSub); + } + this.handlerToSubscribtion.remove(subscribtion.getHandler(), subscribtion); + } + this.checkSubscribtions(topic, future); + return future; + } + + /** + * Publish a message to the given payload + * + * @param topic The topic to publish to + * @param payload The payload to send + * @return A future which will be completed when the message is sent out of the MqttClient + */ + @Override + public Future publish(String topic, ByteBuf payload) { + return publish(topic, payload, MqttQoS.AT_MOST_ONCE, false); + } + + /** + * Publish a message to the given payload, using the given qos + * + * @param topic The topic to publish to + * @param payload The payload to send + * @param qos The qos to use while publishing + * @return A future which will be completed when the message is delivered to the server + */ + @Override + public Future publish(String topic, ByteBuf payload, MqttQoS qos) { + return publish(topic, payload, qos, false); + } + + /** + * Publish a message to the given payload, using optional retain + * + * @param topic The topic to publish to + * @param payload The payload to send + * @param retain true if you want to retain the message on the server, false otherwise + * @return A future which will be completed when the message is sent out of the MqttClient + */ + @Override + public Future publish(String topic, ByteBuf payload, boolean retain) { + return publish(topic, payload, MqttQoS.AT_MOST_ONCE, retain); + } + + /** + * Publish a message to the given payload, using the given qos and optional retain + * + * @param topic The topic to publish to + * @param payload The payload to send + * @param qos The qos to use while publishing + * @param retain true if you want to retain the message on the server, false otherwise + * @return A future which will be completed when the message is delivered to the server + */ + @Override + public Future publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain) { + Promise future = new DefaultPromise<>(this.eventLoop.next()); + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retain, 0); + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId()); + MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload); + MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.messageId(), future, payload.retain(), message, qos); + ChannelFuture channelFuture = this.sendAndFlushPacket(message); + + if (channelFuture != null) { + pendingPublish.setSent(channelFuture != null); + if (channelFuture.cause() != null) { + future.setFailure(channelFuture.cause()); + return future; + } + } + if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { + pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 + } else if (pendingPublish.isSent()) { + this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); + pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); + } + return future; + } + + /** + * Retrieve the MqttClient configuration + * + * @return The {@link MqttClientConfig} instance we use + */ + @Override + public MqttClientConfig getClientConfig() { + return clientConfig; + } + + @Override + public void disconnect() { + disconnected = true; + if (this.channel != null) { + MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); + this.sendAndFlushPacket(message).addListener(future1 -> channel.close()); + } + } + + @Override + public void setCallback(MqttClientCallback callback) { + this.callback = callback; + } + + + ///////////////////////////////////////////// PRIVATE API ///////////////////////////////////////////// + + ChannelFuture sendAndFlushPacket(Object message) { + if (this.channel == null) { + return null; + } + if (this.channel.isActive()) { + return this.channel.writeAndFlush(message); + } + ChannelClosedException e = new ChannelClosedException("Channel is closed"); + if (callback != null) { + callback.connectionLost(e); + } + return this.channel.newFailedFuture(e); + } + + private MqttMessageIdVariableHeader getNewMessageId() { + this.nextMessageId.compareAndSet(0xffff, 1); + return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement()); + } + + private Future createSubscribtion(String topic, MqttHandler handler, boolean once, MqttQoS qos) { + if (this.pendingSubscribeTopics.contains(topic)) { + Optional> subscribtionEntry = this.pendingSubscribtions.entrySet().stream().filter((e) -> e.getValue().getTopic().equals(topic)).findAny(); + if (subscribtionEntry.isPresent()) { + subscribtionEntry.get().getValue().addHandler(handler, once); + return subscribtionEntry.get().getValue().getFuture(); + } + } + if (this.serverSubscribtions.contains(topic)) { + MqttSubscribtion subscribtion = new MqttSubscribtion(topic, handler, once); + this.subscriptions.put(topic, subscribtion); + this.handlerToSubscribtion.put(handler, subscribtion); + return this.channel.newSucceededFuture(); + } + + Promise future = new DefaultPromise<>(this.eventLoop.next()); + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); + MqttTopicSubscription subscription = new MqttTopicSubscription(topic, qos); + MqttMessageIdVariableHeader variableHeader = getNewMessageId(); + MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription)); + MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload); + + final MqttPendingSubscribtion pendingSubscribtion = new MqttPendingSubscribtion(future, topic, message); + pendingSubscribtion.addHandler(handler, once); + this.pendingSubscribtions.put(variableHeader.messageId(), pendingSubscribtion); + this.pendingSubscribeTopics.add(topic); + pendingSubscribtion.setSent(this.sendAndFlushPacket(message) != null); //If not sent, we will send it when the connection is opened + + pendingSubscribtion.startRetransmitTimer(this.eventLoop.next(), this::sendAndFlushPacket); + + return future; + } + + private void checkSubscribtions(String topic, Promise promise) { + if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscribtions.contains(topic)) { + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); + MqttMessageIdVariableHeader variableHeader = getNewMessageId(); + MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); + MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload); + + MqttPendingUnsubscribtion pendingUnsubscribtion = new MqttPendingUnsubscribtion(promise, topic, message); + this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscribtion); + pendingUnsubscribtion.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); + + this.sendAndFlushPacket(message); + } else { + promise.setSuccess(null); + } + } + + IntObjectHashMap getPendingSubscribtions() { + return pendingSubscribtions; + } + + HashMultimap getSubscriptions() { + return subscriptions; + } + + Set getPendingSubscribeTopics() { + return pendingSubscribeTopics; + } + + HashMultimap getHandlerToSubscribtion() { + return handlerToSubscribtion; + } + + Set getServerSubscribtions() { + return serverSubscribtions; + } + + IntObjectHashMap getPendingServerUnsubscribes() { + return pendingServerUnsubscribes; + } + + IntObjectHashMap getPendingPublishes() { + return pendingPublishes; + } + + IntObjectHashMap getQos2PendingIncomingPublishes() { + return qos2PendingIncomingPublishes; + } + + private class MqttChannelInitializer extends ChannelInitializer { + + private final Promise connectFuture; + private final String host; + private final int port; + private final SslContext sslContext; + + + public MqttChannelInitializer(Promise connectFuture, String host, int port, SslContext sslContext) { + this.connectFuture = connectFuture; + this.host = host; + this.port = port; + this.sslContext = sslContext; + } + + @Override + protected void initChannel(SocketChannel ch) throws Exception { + if (sslContext != null) { + ch.pipeline().addLast(sslContext.newHandler(ch.alloc(), host, port)); + } + + ch.pipeline().addLast("mqttDecoder", new MqttDecoder()); + ch.pipeline().addLast("mqttEncoder", MqttEncoder.INSTANCE); + ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds(), MqttClientImpl.this.clientConfig.getTimeoutSeconds(), 0)); + ch.pipeline().addLast("mqttPingHandler", new MqttPingHandler(MqttClientImpl.this.clientConfig.getTimeoutSeconds())); + ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture)); + } + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java new file mode 100644 index 0000000000..5fa0e6da43 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.ChannelFuture; +import io.netty.handler.codec.mqtt.MqttConnectReturnCode; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class MqttConnectResult { + + private final boolean success; + private final MqttConnectReturnCode returnCode; + private final ChannelFuture closeFuture; + + MqttConnectResult(boolean success, MqttConnectReturnCode returnCode, ChannelFuture closeFuture) { + this.success = success; + this.returnCode = returnCode; + this.closeFuture = closeFuture; + } + + public boolean isSuccess() { + return success; + } + + public MqttConnectReturnCode getReturnCode() { + return returnCode; + } + + public ChannelFuture getCloseFuture() { + return closeFuture; + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java new file mode 100644 index 0000000000..4d6d58cf1a --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.buffer.ByteBuf; + +public interface MqttHandler { + + void onMessage(String topic, ByteBuf payload); +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java new file mode 100644 index 0000000000..af84cde14f --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.mqtt.*; + +import java.util.function.Consumer; + +final class MqttIncomingQos2Publish { + + private final MqttPublishMessage incomingPublish; + + private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); + + MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) { + this.incomingPublish = incomingPublish; + + this.retransmissionHandler.setOriginalMessage(originalMessage); + } + + MqttPublishMessage getIncomingPublish() { + return incomingPublish; + } + + void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { + this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> + sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader()))); + this.retransmissionHandler.start(eventLoop); + } + + void onPubrelReceived() { + this.retransmissionHandler.stop(); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java new file mode 100644 index 0000000000..1dadfcda8c --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java @@ -0,0 +1,154 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.handler.codec.mqtt.MqttQoS; + +@SuppressWarnings({"WeakerAccess", "unused", "SimplifiableIfStatement", "StringBufferReplaceableByString"}) +public final class MqttLastWill { + + private final String topic; + private final String message; + private final boolean retain; + private final MqttQoS qos; + + public MqttLastWill(String topic, String message, boolean retain, MqttQoS qos) { + if(topic == null){ + throw new NullPointerException("topic"); + } + if(message == null){ + throw new NullPointerException("message"); + } + if(qos == null){ + throw new NullPointerException("qos"); + } + this.topic = topic; + this.message = message; + this.retain = retain; + this.qos = qos; + } + + public String getTopic() { + return topic; + } + + public String getMessage() { + return message; + } + + public boolean isRetain() { + return retain; + } + + public MqttQoS getQos() { + return qos; + } + + public static MqttLastWill.Builder builder(){ + return new MqttLastWill.Builder(); + } + + public static final class Builder { + + private String topic; + private String message; + private boolean retain; + private MqttQoS qos; + + public String getTopic() { + return topic; + } + + public Builder setTopic(String topic) { + if(topic == null){ + throw new NullPointerException("topic"); + } + this.topic = topic; + return this; + } + + public String getMessage() { + return message; + } + + public Builder setMessage(String message) { + if(message == null){ + throw new NullPointerException("message"); + } + this.message = message; + return this; + } + + public boolean isRetain() { + return retain; + } + + public Builder setRetain(boolean retain) { + this.retain = retain; + return this; + } + + public MqttQoS getQos() { + return qos; + } + + public Builder setQos(MqttQoS qos) { + if(qos == null){ + throw new NullPointerException("qos"); + } + this.qos = qos; + return this; + } + + public MqttLastWill build(){ + return new MqttLastWill(topic, message, retain, qos); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MqttLastWill that = (MqttLastWill) o; + + if (retain != that.retain) return false; + if (!topic.equals(that.topic)) return false; + if (!message.equals(that.message)) return false; + return qos == that.qos; + + } + + @Override + public int hashCode() { + int result = topic.hashCode(); + result = 31 * result + message.hashCode(); + result = 31 * result + (retain ? 1 : 0); + result = 31 * result + qos.hashCode(); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MqttLastWill{"); + sb.append("topic='").append(topic).append('\''); + sb.append(", message='").append(message).append('\''); + sb.append(", retain=").append(retain); + sb.append(", qos=").append(qos.name()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java new file mode 100644 index 0000000000..c656e84fdf --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.util.concurrent.Promise; + +import java.util.function.Consumer; + +final class MqttPendingPublish { + + private final int messageId; + private final Promise future; + private final ByteBuf payload; + private final MqttPublishMessage message; + private final MqttQoS qos; + + private final RetransmissionHandler publishRetransmissionHandler = new RetransmissionHandler<>(); + private final RetransmissionHandler pubrelRetransmissionHandler = new RetransmissionHandler<>(); + + private boolean sent = false; + + MqttPendingPublish(int messageId, Promise future, ByteBuf payload, MqttPublishMessage message, MqttQoS qos) { + this.messageId = messageId; + this.future = future; + this.payload = payload; + this.message = message; + this.qos = qos; + + this.publishRetransmissionHandler.setOriginalMessage(message); + } + + int getMessageId() { + return messageId; + } + + Promise getFuture() { + return future; + } + + ByteBuf getPayload() { + return payload; + } + + boolean isSent() { + return sent; + } + + void setSent(boolean sent) { + this.sent = sent; + } + + MqttPublishMessage getMessage() { + return message; + } + + MqttQoS getQos() { + return qos; + } + + void startPublishRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { + this.publishRetransmissionHandler.setHandle(((fixedHeader, originalMessage) -> + sendPacket.accept(new MqttPublishMessage(fixedHeader, originalMessage.variableHeader(), this.payload.retain())))); + this.publishRetransmissionHandler.start(eventLoop); + } + + void onPubackReceived() { + this.publishRetransmissionHandler.stop(); + } + + void setPubrelMessage(MqttMessage pubrelMessage) { + this.pubrelRetransmissionHandler.setOriginalMessage(pubrelMessage); + } + + void startPubrelRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { + this.pubrelRetransmissionHandler.setHandle((fixedHeader, originalMessage) -> + sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader()))); + this.pubrelRetransmissionHandler.start(eventLoop); + } + + void onPubcompReceived() { + this.pubrelRetransmissionHandler.stop(); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java new file mode 100644 index 0000000000..782aef1e12 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.mqtt.MqttSubscribeMessage; +import io.netty.util.concurrent.Promise; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +final class MqttPendingSubscribtion { + + private final Promise future; + private final String topic; + private final Set handlers = new HashSet<>(); + private final MqttSubscribeMessage subscribeMessage; + + private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); + + private boolean sent = false; + + MqttPendingSubscribtion(Promise future, String topic, MqttSubscribeMessage message) { + this.future = future; + this.topic = topic; + this.subscribeMessage = message; + + this.retransmissionHandler.setOriginalMessage(message); + } + + Promise getFuture() { + return future; + } + + String getTopic() { + return topic; + } + + boolean isSent() { + return sent; + } + + void setSent(boolean sent) { + this.sent = sent; + } + + MqttSubscribeMessage getSubscribeMessage() { + return subscribeMessage; + } + + void addHandler(MqttHandler handler, boolean once){ + this.handlers.add(new MqttPendingHandler(handler, once)); + } + + Set getHandlers() { + return handlers; + } + + void startRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { + if(this.sent){ //If the packet is sent, we can start the retransmit timer + this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> + sendPacket.accept(new MqttSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()))); + this.retransmissionHandler.start(eventLoop); + } + } + + void onSubackReceived(){ + this.retransmissionHandler.stop(); + } + + final class MqttPendingHandler { + private final MqttHandler handler; + private final boolean once; + + MqttPendingHandler(MqttHandler handler, boolean once) { + this.handler = handler; + this.once = once; + } + + MqttHandler getHandler() { + return handler; + } + + boolean isOnce() { + return once; + } + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java new file mode 100644 index 0000000000..a626a815e4 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; +import io.netty.util.concurrent.Promise; + +import java.util.function.Consumer; + +final class MqttPendingUnsubscribtion { + + private final Promise future; + private final String topic; + + private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); + + MqttPendingUnsubscribtion(Promise future, String topic, MqttUnsubscribeMessage unsubscribeMessage) { + this.future = future; + this.topic = topic; + + this.retransmissionHandler.setOriginalMessage(unsubscribeMessage); + } + + Promise getFuture() { + return future; + } + + String getTopic() { + return topic; + } + + void startRetransmissionTimer(EventLoop eventLoop, Consumer sendPacket) { + this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> + sendPacket.accept(new MqttUnsubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()))); + this.retransmissionHandler.start(eventLoop); + } + + void onUnsubackReceived(){ + this.retransmissionHandler.stop(); + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java new file mode 100644 index 0000000000..d0fd998d69 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttMessageType; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.ScheduledFuture; + +import java.util.concurrent.TimeUnit; + +final class MqttPingHandler extends ChannelInboundHandlerAdapter { + + private final int keepaliveSeconds; + + private ScheduledFuture pingRespTimeout; + + MqttPingHandler(int keepaliveSeconds) { + this.keepaliveSeconds = keepaliveSeconds; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (!(msg instanceof MqttMessage)) { + ctx.fireChannelRead(msg); + return; + } + MqttMessage message = (MqttMessage) msg; + if(message.fixedHeader().messageType() == MqttMessageType.PINGREQ){ + this.handlePingReq(ctx.channel()); + } else if(message.fixedHeader().messageType() == MqttMessageType.PINGRESP){ + this.handlePingResp(); + }else{ + ctx.fireChannelRead(ReferenceCountUtil.retain(msg)); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + super.userEventTriggered(ctx, evt); + + if(evt instanceof IdleStateEvent){ + IdleStateEvent event = (IdleStateEvent) evt; + switch(event.state()){ + case READER_IDLE: + break; + case WRITER_IDLE: + this.sendPingReq(ctx.channel()); + break; + } + } + } + + private void sendPingReq(Channel channel){ + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGREQ, false, MqttQoS.AT_MOST_ONCE, false, 0); + channel.writeAndFlush(new MqttMessage(fixedHeader)); + + if(this.pingRespTimeout != null){ + this.pingRespTimeout = channel.eventLoop().schedule(() -> { + MqttFixedHeader fixedHeader2 = new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0); + channel.writeAndFlush(new MqttMessage(fixedHeader2)).addListener(ChannelFutureListener.CLOSE); + //TODO: what do when the connection is closed ? + }, this.keepaliveSeconds, TimeUnit.SECONDS); + } + } + + private void handlePingReq(Channel channel){ + MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0); + channel.writeAndFlush(new MqttMessage(fixedHeader)); + } + + private void handlePingResp(){ + if(this.pingRespTimeout != null && !this.pingRespTimeout.isCancelled() && !this.pingRespTimeout.isDone()){ + this.pingRespTimeout.cancel(true); + this.pingRespTimeout = null; + } + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java new file mode 100644 index 0000000000..27f4cb969e --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import java.util.regex.Pattern; + +final class MqttSubscribtion { + + private final String topic; + private final Pattern topicRegex; + private final MqttHandler handler; + + private final boolean once; + + private boolean called; + + MqttSubscribtion(String topic, MqttHandler handler, boolean once) { + if(topic == null){ + throw new NullPointerException("topic"); + } + if(handler == null){ + throw new NullPointerException("handler"); + } + this.topic = topic; + this.handler = handler; + this.once = once; + this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); + } + + String getTopic() { + return topic; + } + + public MqttHandler getHandler() { + return handler; + } + + boolean isOnce() { + return once; + } + + boolean isCalled() { + return called; + } + + boolean matches(String topic){ + return this.topicRegex.matcher(topic).matches(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MqttSubscribtion that = (MqttSubscribtion) o; + + return once == that.once && topic.equals(that.topic) && handler.equals(that.handler); + } + + @Override + public int hashCode() { + int result = topic.hashCode(); + result = 31 * result + handler.hashCode(); + result = 31 * result + (once ? 1 : 0); + return result; + } + + void setCalled(boolean called) { + this.called = called; + } +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java new file mode 100644 index 0000000000..36e91e5210 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2018 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.mqtt; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.mqtt.MqttFixedHeader; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.util.concurrent.ScheduledFuture; + +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +final class RetransmissionHandler { + + private ScheduledFuture timer; + private int timeout = 10; + private BiConsumer handler; + private T originalMessage; + + void start(EventLoop eventLoop){ + if(eventLoop == null){ + throw new NullPointerException("eventLoop"); + } + if(this.handler == null){ + throw new NullPointerException("handler"); + } + this.timeout = 10; + this.startTimer(eventLoop); + } + + private void startTimer(EventLoop eventLoop){ + this.timer = eventLoop.schedule(() -> { + this.timeout += 5; + MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), true, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); + handler.accept(fixedHeader, originalMessage); + startTimer(eventLoop); + }, timeout, TimeUnit.SECONDS); + } + + void stop(){ + if(this.timer != null){ + this.timer.cancel(true); + } + } + + void setHandle(BiConsumer runnable) { + this.handler = runnable; + } + + void setOriginalMessage(T originalMessage) { + this.originalMessage = originalMessage; + } +} diff --git a/pom.xml b/pom.xml index 67d48b268b..297512fc9a 100755 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,6 @@ 2.5.3 1.2.1 9.4.1211 - 2.0.0TB org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* @@ -87,6 +86,7 @@ + netty-mqtt common rule-engine dao @@ -324,6 +324,11 @@ + + org.thingsboard + netty-mqtt + ${project.version} + org.thingsboard extensions-api @@ -568,6 +573,11 @@ netty-handler ${netty.version} + + io.netty + netty-codec-mqtt + ${netty.version} + com.datastax.cassandra cassandra-driver-core @@ -819,11 +829,6 @@ exe provided - - nl.jk5.netty-mqtt - netty-mqtt - ${netty-mqtt-client.version} - org.elasticsearch.client rest diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 3352f5acec..0ef9f729e1 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -68,6 +68,10 @@ org.thingsboard.rule-engine rule-engine-api + + org.thingsboard + netty-mqtt + com.google.guava guava @@ -90,10 +94,6 @@ com.rabbitmq amqp-client - - nl.jk5.netty-mqtt - netty-mqtt - org.bouncycastle bcpkix-jdk15on diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 9694b0498b..ce54a73f4b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -24,9 +24,9 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; -import nl.jk5.mqtt.MqttClient; -import nl.jk5.mqtt.MqttClientConfig; -import nl.jk5.mqtt.MqttConnectResult; +import org.thingsboard.mqtt.MqttClient; +import org.thingsboard.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttConnectResult; import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.api.*; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java index 9d5e8df9b5..53aea9604c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.mqtt.credentials; import io.netty.handler.ssl.SslContext; -import nl.jk5.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttClientConfig; import java.util.Optional; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java index cbbd7036a2..b3d86c6a07 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java @@ -18,7 +18,7 @@ package org.thingsboard.rule.engine.mqtt.credentials; import io.netty.handler.ssl.SslContext; import lombok.Data; -import nl.jk5.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttClientConfig; import java.util.Optional; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java index c9fb4a32a9..a462839b1e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java @@ -22,7 +22,7 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import nl.jk5.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttClientConfig; import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMDecryptorProvider; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java index 5c4594ff44..0ab81a8457 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java @@ -19,7 +19,7 @@ package org.thingsboard.rule.engine.mqtt.credentials; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.netty.handler.ssl.SslContext; -import nl.jk5.mqtt.MqttClientConfig; +import org.thingsboard.mqtt.MqttClientConfig; import java.util.Optional; From cbceca66f8d621b9bf4d59aaf8bff0fa8529e067 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 14 May 2018 19:26:40 +0300 Subject: [PATCH 4/5] Add latest debug input information to test script dialog. Add device attributes node. --- .../server/controller/BaseController.java | 2 +- .../controller/RuleChainController.java | 34 ++++++ .../server/dao/event/BaseEventService.java | 5 + .../dao/event/CassandraBaseEventDao.java | 15 +++ .../server/dao/event/EventDao.java | 12 ++ .../server/dao/event/EventService.java | 5 + .../server/dao/sql/event/EventRepository.java | 16 +++ .../server/dao/sql/event/JpaBaseEventDao.java | 11 ++ .../thingsboard/rule/engine/DonAsynchron.java | 19 +++- .../rule/engine/action/TbClearAlarmNode.java | 2 +- .../engine/data/DeviceRelationsQuery.java | 29 +++++ .../filter/TbJsSwitchNodeConfiguration.java | 2 +- .../metadata/TbAbstractGetAttributesNode.java | 103 ++++++++++++++++++ .../engine/metadata/TbGetAttributesNode.java | 62 ++--------- .../engine/metadata/TbGetDeviceAttrNode.java | 52 +++++++++ .../TbGetDeviceAttrNodeConfiguration.java | 48 ++++++++ .../TbGetEntityAttrNodeConfiguration.java | 2 +- .../TbGetRelatedAttrNodeConfiguration.java | 2 +- .../EntitiesRelatedDeviceIdAsyncLoader.java | 56 ++++++++++ .../static/rulenode/rulenode-core-config.js | 6 +- ui/src/app/api/rule-chain.service.js | 14 ++- .../rulechain/rulenode-config.directive.js | 1 + ui/src/app/rulechain/rulenode-config.tpl.html | 1 + .../rulenode-defined-config.directive.js | 3 +- .../app/rulechain/rulenode-fieldset.tpl.html | 1 + .../script/node-script-test.service.js | 67 +++++++++++- .../script/node-script-test.tpl.html | 2 +- 27 files changed, 501 insertions(+), 71 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index f24c77a376..4441fb8d6c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -607,7 +607,7 @@ public abstract class BaseController { } public static Exception toException(Throwable error) { - return Exception.class.isInstance(error) ? (Exception) error : new Exception(error); + return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null; } } diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 05e7836786..81e5179838 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -21,14 +21,18 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.thingsboard.rule.engine.api.ScriptEngine; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; @@ -38,6 +42,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.service.script.NashornJsEngine; @@ -52,9 +57,13 @@ import java.util.Set; public class RuleChainController extends BaseController { public static final String RULE_CHAIN_ID = "ruleChainId"; + public static final String RULE_NODE_ID = "ruleNodeId"; private static final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private EventService eventService; + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @ResponseBody @@ -217,6 +226,31 @@ public class RuleChainController extends BaseController { } } + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) + @ResponseBody + public JsonNode getLatestRuleNodeDebugInput(@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { + checkParameter(RULE_NODE_ID, strRuleNodeId); + try { + RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); + TenantId tenantId = getCurrentUser().getTenantId(); + List events = eventService.findLatestEvents(tenantId, ruleNodeId, DataConstants.DEBUG_RULE_NODE, 2); + JsonNode result = null; + if (events != null) { + for (Event event : events) { + JsonNode body = event.getBody(); + if (body.has("type") && body.get("type").asText().equals("IN")) { + result = body; + break; + } + } + } + return result; + } catch (Exception e) { + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @ResponseBody diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java index b8ba4a73fb..55da480079 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java @@ -82,6 +82,11 @@ public class BaseEventService implements EventService { return new TimePageData<>(events, pageLink); } + @Override + public List findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) { + return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit); + } + private DataValidator eventValidator = new DataValidator() { @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java index 43d3fd50e7..e143ab189b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java @@ -134,6 +134,21 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { + log.trace("Try to find latest events by tenant [{}], entity [{}], type [{}] and limit [{}]", tenantId, entityId, eventType, limit); + Select select = select().from(EVENT_BY_TYPE_AND_ID_VIEW_NAME); + Select.Where query = select.where(); + query.and(eq(ModelConstants.EVENT_TENANT_ID_PROPERTY, tenantId)); + query.and(eq(ModelConstants.EVENT_ENTITY_TYPE_PROPERTY, entityId.getEntityType())); + query.and(eq(ModelConstants.EVENT_ENTITY_ID_PROPERTY, entityId.getId())); + query.and(eq(ModelConstants.EVENT_TYPE_PROPERTY, eventType)); + query.limit(limit); + query.orderBy(QueryBuilder.desc(ModelConstants.EVENT_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY)); + List entities = findListByStatement(query); + return DaoUtil.convertDataList(entities); + } + private Optional save(EventEntity entity, boolean ifNotExists) { if (entity.getId() == null) { entity.setId(UUIDs.timeBased()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java index fb5c0fba8f..eb0fdbcd80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java @@ -76,4 +76,16 @@ public interface EventDao extends Dao { * @return the event list */ List findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + + /** + * Find latest events by tenantId, entityId and eventType. + * + * @param tenantId the tenantId + * @param entityId the entityId + * @param eventType the eventType + * @param limit the limit + * @return the event list + */ + List findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java index edee19033f..b6ef4a3e04 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/event/EventService.java @@ -21,7 +21,9 @@ 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 java.util.List; import java.util.Optional; +import java.util.UUID; public interface EventService { @@ -34,4 +36,7 @@ public interface EventService { TimePageData findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink); TimePageData findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink); + + List findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index 89cd9444d7..d14b7bf530 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -15,12 +15,18 @@ */ package org.thingsboard.server.dao.sql.event; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.EventEntity; import org.thingsboard.server.dao.util.SqlDao; +import java.util.List; + /** * Created by Valerii Sosliuk on 5/3/2017. */ @@ -36,4 +42,14 @@ public interface EventRepository extends CrudRepository, Jp EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId, EntityType entityType, String entityId); + + @Query("SELECT e FROM EventEntity e WHERE e.tenantId = :tenantId AND e.entityType = :entityType " + + "AND e.entityId = :entityId AND e.eventType = :eventType ORDER BY e.eventType DESC, e.id DESC") + List findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( + @Param("tenantId") String tenantId, + @Param("entityType") EntityType entityType, + @Param("entityId") String entityId, + @Param("eventType") String eventType, + Pageable pageable); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 9b22773e1e..d1d64f0636 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -109,6 +109,17 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) { + List latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType( + UUIDConverter.fromTimeUUID(tenantId), + entityId.getEntityType(), + UUIDConverter.fromTimeUUID(entityId.getId()), + eventType, + new PageRequest(0, limit)); + return DaoUtil.convertDataList(latest); + } + public Optional save(EventEntity entity, boolean ifNotExists) { log.debug("Save event [{}] ", entity); if (entity.getTenantId() == null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java index 4fed574638..e697a84b74 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java @@ -20,12 +20,19 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import javax.annotation.Nullable; +import java.util.concurrent.Executor; import java.util.function.Consumer; public class DonAsynchron { - public static void withCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { - Futures.addCallback(future, new FutureCallback() { + public static void withCallback(ListenableFuture future, Consumer onSuccess, + Consumer onFailure) { + withCallback(future, onSuccess, onFailure, null); + } + + public static void withCallback(ListenableFuture future, Consumer onSuccess, + Consumer onFailure, Executor executor) { + FutureCallback callback = new FutureCallback() { @Override public void onSuccess(@Nullable T result) { try { @@ -33,13 +40,17 @@ public class DonAsynchron { } catch (Throwable th) { onFailure(th); } - } @Override public void onFailure(Throwable t) { onFailure.accept(t); } - }); + }; + if (executor != null) { + Futures.addCallback(future, callback, executor); + } else { + Futures.addCallback(future, callback); + } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index ce509fcbb3..d223f4dd4a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.msg.TbMsg; "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeClearAlarmConfig", - icon = "notifications_active" + icon = "notifications_off" ) public class TbClearAlarmNode extends TbAbstractAlarmNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java new file mode 100644 index 0000000000..dca7b38bb3 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2018 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.rule.engine.data; + +import lombok.Data; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +import java.util.List; + +@Data +public class DeviceRelationsQuery { + private EntitySearchDirection direction; + private int maxLevel = 1; + private String relationType; + private List deviceTypes; +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java index 671132d8f9..d1dba1bd3c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java @@ -32,7 +32,7 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration implements TbNode { + + protected C config; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = loadGetAttributesNodeConfig(configuration); + } + + protected abstract C loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; + + @Override + public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { + try { + withCallback( + findEntityAsync(ctx, msg.getOriginator()), + entityId -> safePutAttributes(ctx, msg, entityId), + t -> ctx.tellError(msg, t), ctx.getDbCallbackExecutor()); + } catch (Throwable th) { + ctx.tellError(msg, th); + } + } + + private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) { + if(entityId == null || entityId.isNullUid()) { + ctx.tellNext(msg, FAILURE); + return; + } + ListenableFuture> allFutures = Futures.allAsList( + putLatestTelemetry(ctx, entityId, msg, config.getLatestTsKeyNames()), + putAttrAsync(ctx, entityId, msg, CLIENT_SCOPE, config.getClientAttributeNames(), "cs_"), + putAttrAsync(ctx, entityId, msg, SHARED_SCOPE, config.getSharedAttributeNames(), "shared_"), + putAttrAsync(ctx, entityId, msg, SERVER_SCOPE, config.getServerAttributeNames(), "ss_") + ); + withCallback(allFutures, i -> ctx.tellNext(msg, SUCCESS), t -> ctx.tellError(msg, t)); + } + + private ListenableFuture putAttrAsync(TbContext ctx, EntityId entityId, TbMsg msg, String scope, List keys, String prefix) { + if (CollectionUtils.isEmpty(keys)) { + return Futures.immediateFuture(null); + } + ListenableFuture> latest = ctx.getAttributesService().find(entityId, scope, keys); + return Futures.transform(latest, (Function, Void>) l -> { + l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString())); + return null; + }); + } + + private ListenableFuture putLatestTelemetry(TbContext ctx, EntityId entityId, TbMsg msg, List keys) { + if (CollectionUtils.isEmpty(keys)) { + return Futures.immediateFuture(null); + } + ListenableFuture> latest = ctx.getTimeseriesService().findLatest(entityId, keys); + return Futures.transform(latest, (Function, Void>) l -> { + l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString())); + return null; + }); + } + + @Override + public void destroy() { + + } + + protected abstract ListenableFuture findEntityAsync(TbContext ctx, EntityId originator); +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 06a0f44f18..51434304ef 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -15,23 +15,16 @@ */ package org.thingsboard.rule.engine.metadata; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections.CollectionUtils; import org.thingsboard.rule.engine.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.common.msg.TbMsg; - -import java.util.List; - -import static org.thingsboard.rule.engine.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; -import static org.thingsboard.server.common.data.DataConstants.*; /** * Created by ashvayka on 19.01.18. @@ -47,50 +40,15 @@ import static org.thingsboard.server.common.data.DataConstants.*; "metadata.cs_temperature or metadata.shared_limit ", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") -public class TbGetAttributesNode implements TbNode { - - private TbGetAttributesNodeConfiguration config; +public class TbGetAttributesNode extends TbAbstractGetAttributesNode { @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); + protected TbGetAttributesNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException { + return TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); } @Override - public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { - ListenableFuture> allFutures = Futures.allAsList( - putLatestTelemetry(ctx, msg, config.getLatestTsKeyNames()), - putAttrAsync(ctx, msg, CLIENT_SCOPE, config.getClientAttributeNames(), "cs_"), - putAttrAsync(ctx, msg, SHARED_SCOPE, config.getSharedAttributeNames(), "shared_"), - putAttrAsync(ctx, msg, SERVER_SCOPE, config.getServerAttributeNames(), "ss_") - ); - withCallback(allFutures, i -> ctx.tellNext(msg, SUCCESS), t -> ctx.tellError(msg, t)); - } - - private ListenableFuture putAttrAsync(TbContext ctx, TbMsg msg, String scope, List keys, String prefix) { - if (CollectionUtils.isEmpty(keys)) { - return Futures.immediateFuture(null); - } - ListenableFuture> latest = ctx.getAttributesService().find(msg.getOriginator(), scope, keys); - return Futures.transform(latest, (Function, Void>) l -> { - l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString())); - return null; - }); - } - - private ListenableFuture putLatestTelemetry(TbContext ctx, TbMsg msg, List keys) { - if (CollectionUtils.isEmpty(keys)) { - return Futures.immediateFuture(null); - } - ListenableFuture> latest = ctx.getTimeseriesService().findLatest(msg.getOriginator(), keys); - return Futures.transform(latest, (Function, Void>) l -> { - l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString())); - return null; - }); - } - - @Override - public void destroy() { - + protected ListenableFuture findEntityAsync(TbContext ctx, EntityId originator) { + return Futures.immediateFuture(originator); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java new file mode 100644 index 0000000000..983a08a5b1 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2018 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.rule.engine.metadata; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.TbNodeUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.plugin.ComponentType; + +@Slf4j +@RuleNode(type = ComponentType.ENRICHMENT, + name = "device attributes", + configClazz = TbGetDeviceAttrNodeConfiguration.class, + nodeDescription = "Add Originators Related Device Attributes or Latest Telemetry into Message Metadata", + nodeDetails = "If Attributes enrichment configured, CLIENT/SHARED/SERVER attributes are added into Message metadata " + + "with specific prefix: cs/shared/ss. Latest telemetry value added into metadata without prefix. " + + "To access those attributes in other nodes this template can be used " + + "metadata.cs_temperature or metadata.shared_limit ", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbEnrichmentNodeDeviceAttributesConfig") +public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode { + + @Override + protected TbGetDeviceAttrNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException { + return TbNodeUtils.convert(configuration, TbGetDeviceAttrNodeConfiguration.class); + } + + @Override + protected ListenableFuture findEntityAsync(TbContext ctx, EntityId originator) { + return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, originator, config.getDeviceRelationsQuery()); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java new file mode 100644 index 0000000000..4d8307c072 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2018 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.rule.engine.metadata; + +import lombok.Data; +import org.thingsboard.rule.engine.data.DeviceRelationsQuery; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +import java.util.Collections; + +@Data +public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfiguration { + + private DeviceRelationsQuery deviceRelationsQuery; + + @Override + public TbGetDeviceAttrNodeConfiguration defaultConfiguration() { + TbGetDeviceAttrNodeConfiguration configuration = new TbGetDeviceAttrNodeConfiguration(); + configuration.setClientAttributeNames(Collections.emptyList()); + configuration.setSharedAttributeNames(Collections.emptyList()); + configuration.setServerAttributeNames(Collections.emptyList()); + configuration.setLatestTsKeyNames(Collections.emptyList()); + + DeviceRelationsQuery deviceRelationsQuery = new DeviceRelationsQuery(); + deviceRelationsQuery.setDirection(EntitySearchDirection.FROM); + deviceRelationsQuery.setMaxLevel(1); + deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE); + deviceRelationsQuery.setDeviceTypes(Collections.singletonList("default")); + + configuration.setDeviceRelationsQuery(deviceRelationsQuery); + + return configuration; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java index 0bcefae872..a1029ba894 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java @@ -34,7 +34,7 @@ public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration attrMapping = new HashMap<>(); attrMapping.putIfAbsent("temperature", "tempo"); configuration.setAttrMapping(attrMapping); - configuration.setTelemetry(true); + configuration.setTelemetry(false); return configuration; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java index dccd87879a..63186ca7e9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java @@ -36,7 +36,7 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig Map attrMapping = new HashMap<>(); attrMapping.putIfAbsent("temperature", "tempo"); configuration.setAttrMapping(attrMapping); - configuration.setTelemetry(true); + configuration.setTelemetry(false); RelationsQuery relationsQuery = new RelationsQuery(); relationsQuery.setDirection(EntitySearchDirection.FROM); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java new file mode 100644 index 0000000000..8a09504e25 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2018 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.rule.engine.util; + +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.apache.commons.collections.CollectionUtils; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.data.DeviceRelationsQuery; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.DeviceSearchQuery; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.dao.device.DeviceService; + +import java.util.List; + +public class EntitiesRelatedDeviceIdAsyncLoader { + + public static ListenableFuture findDeviceAsync(TbContext ctx, EntityId originator, + DeviceRelationsQuery deviceRelationsQuery) { + DeviceService deviceService = ctx.getDeviceService(); + DeviceSearchQuery query = buildQuery(originator, deviceRelationsQuery); + + ListenableFuture> asyncDevices = deviceService.findDevicesByQuery(query); + + return Futures.transform(asyncDevices, (AsyncFunction, DeviceId>) + d -> CollectionUtils.isNotEmpty(d) ? Futures.immediateFuture(d.get(0).getId()) + : Futures.immediateFuture(null)); + } + + private static DeviceSearchQuery buildQuery(EntityId originator, DeviceRelationsQuery deviceRelationsQuery) { + DeviceSearchQuery query = new DeviceSearchQuery(); + RelationsSearchParameters parameters = new RelationsSearchParameters(originator, + deviceRelationsQuery.getDirection(), deviceRelationsQuery.getMaxLevel()); + query.setParameters(parameters); + query.setRelationType(deviceRelationsQuery.getRelationType()); + query.setDeviceTypes(deviceRelationsQuery.getDeviceTypes()); + return query; + } +} diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js index 373112ebdd..e32303c668 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1,4 +1,4 @@ -!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),r=e[t[0]];return function(e,t,a){r.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(64)},function(e,t){},1,1,1,function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"; -},20,function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(5),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i,o,"DebugMsg").then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(47),i=r(a),o=n(32),l=r(o),s=n(35),u=r(s),d=n(34),c=r(d),m=n(33),g=r(m),p=n(38),f=r(p),b=n(42),v=r(b),y=n(43),q=r(y),T=n(41),h=r(T),$=n(37),k=r($),w=n(45),C=r(w),x=n(46),_=r(x),M=n(40),S=r(M),E=n(39),V=r(E),N=n(44),P=r(N);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",h.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",_.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",V.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$mdExpansionPanel=t,r.ruleNodeTypes=n,r.credentialsTypeChanged=function(){var e=r.configuration.credentials.type;r.configuration.credentials={},r.configuration.credentials.type=e,r.updateValidity()},r.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){r.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(r.configuration.credentials.caCertFileName=e.name,r.configuration.credentials.caCert=a),"privateKey"==t&&(r.configuration.credentials.privateKeyFileName=e.name,r.configuration.credentials.privateKey=a),"Cert"==t&&(r.configuration.credentials.certFileName=e.name,r.configuration.credentials.cert=a)),r.updateValidity()}})},n.readAsText(e.file)},r.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(r.configuration.credentials.caCertFileName=null,r.configuration.credentials.caCert=null),"privateKey"==e&&(r.configuration.credentials.privateKeyFileName=null,r.configuration.credentials.privateKey=null),"Cert"==e&&(r.configuration.credentials.certFileName=null,r.configuration.credentials.cert=null),r.updateValidity()},r.updateValidity=function(){var e=!0,t=r.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(15),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(19),o=r(i)},function(e,t){"use strict";function n(e){var t=function(t,n,r,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(20),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(51),i=r(a),o=n(52),l=r(o),s=n(49),u=r(s),d=n(53),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",l.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(21),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(22),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(23),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(56),i=r(a),o=n(55),l=r(o),s=n(57),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(27),o=r(i);n(4)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(28),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(29),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(60),i=r(a),o=n(62),l=r(o),s=n(63),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(30),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(31),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(67),i=r(a),o=n(54),l=r(o),s=n(50),u=r(s),d=n(61),c=r(d),m=n(36),g=r(m),p=n(48),f=r(p),b=n(59),v=r(b),y=n(58),q=r(y),T=n(66),h=r(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbKvMapConfig",q.default).config(h.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found", -"no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method",headers:"Headers",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"SMTP protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout (msec)","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){(0,o.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}a.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(65),o=r(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); +!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(68)},function(e,t){},1,1,1,function(e,t){e.exports='
{{scope.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type-required
{{ severity.name | translate}}
tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }}
"},function(e,t){e.exports="
tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.bootstrap-servers-required
tb.rulenode.min-retries-message
tb.rulenode.min-batch-size-bytes-message
tb.rulenode.min-linger-ms-message
tb.rulenode.min-buffer-memory-bytes-message
{{ ackValue }}
tb.rulenode.key-serializer-required
tb.rulenode.value-serializer-required
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports='
tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
tb.rulenode.connect-timeout-required
tb.rulenode.connect-timeout-range
tb.rulenode.connect-timeout-range
{{ \'tb.rulenode.enable-ssl\' | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{credentialsValue.name | translate}}
tb.rulenode.credentials-type-required
tb.rulenode.username-required
tb.rulenode.password-required
'},function(e,t){e.exports='
{{ property }}
tb.rulenode.host-required
tb.rulenode.port-required
tb.rulenode.port-range
tb.rulenode.port-range
{{ \'tb.rulenode.automatic-recovery\' | translate }}
tb.rulenode.min-connection-timeout-ms-message
tb.rulenode.min-handshake-timeout-ms-message
'},function(e,t){e.exports='
tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
{{ type }}
'},function(e,t){e.exports="
"},function(e,t){e.exports="
tb.rulenode.timeout-required
tb.rulenode.min-timeout-message
"},function(e,t){e.exports='
{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}
{{smtpProtocol.toUpperCase()}}
tb.rulenode.smtp-host-required
tb.rulenode.smtp-port-required
tb.rulenode.smtp-port-range
tb.rulenode.smtp-port-range
tb.rulenode.timeout-required
tb.rulenode.min-timeout-msec-message
{{ \'tb.rulenode.enable-tls\' | translate }}
'},function(e,t){e.exports="
tb.rulenode.topic-arn-pattern-required
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
"},function(e,t){e.exports='
{{ type.name | translate }}
tb.rulenode.queue-url-pattern-required
tb.rulenode.min-delay-seconds-message
tb.rulenode.max-delay-seconds-message
tb.rulenode.aws-access-key-id-required
tb.rulenode.aws-secret-access-key-required
tb.rulenode.aws-region-required
'},function(e,t){e.exports="
tb.rulenode.default-ttl-required
tb.rulenode.min-default-ttl-message
"},function(e,t){e.exports='
{{ (\'relation.search-direction.\' + direction) | translate}}
relation.relation-type
device.device-types
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},function(e,t){e.exports='
'; +},function(e,t){e.exports='
'},function(e,t){e.exports="
{{ 'tb.rulenode.latest-telemetry' | translate }}
"},21,function(e,t){e.exports='
{{item}}
tb.rulenode.no-message-types-found
tb.rulenode.no-message-type-matching tb.rulenode.create-new-message-type
{{$chip.name}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports="
{{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports='
{{ keyText }} {{ valText }}  
{{keyRequiredText}}
{{valRequiredText}}
{{ \'tb.key-val.remove-entry\' | translate }} close
{{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports="
{{ ('relation.search-direction.' + direction) | translate}}
relation.relation-filters
"},function(e,t){e.exports='
{{ source.name | translate}}
'},function(e,t){e.exports="
{{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports="
tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(5),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript);a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(49),i=a(r),o=n(34),l=a(o),s=n(37),u=a(s),d=n(36),c=a(d),m=n(35),g=a(m),p=n(40),f=a(p),b=n(44),v=a(b),y=n(45),q=a(y),h=n(43),$=a(h),T=n(39),k=a(T),w=n(47),C=a(w),x=n(48),_=a(x),M=n(42),S=a(M),N=n(41),E=a(N),V=n(46),P=a(V);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",_.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(18),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,r){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(55),i=a(r),o=n(53),l=a(o),s=n(56),u=a(s),d=n(52),c=a(d),m=n(57),g=a(m);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeDeviceAttributesConfig",l.default).directive("tbEnrichmentNodeRelatedAttributesConfig",u.default).directive("tbEnrichmentNodeCustomerAttributesConfig",c.default).directive("tbEnrichmentNodeTenantAttributesConfig",g.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(23),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(24),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(25),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(60),i=a(r),o=n(59),l=a(o),s=n(61),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i);n(4)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{ +value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(31),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(64),i=a(r),o=n(66),l=a(o),s=n(67),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(32),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(33),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(71),i=a(r),o=n(58),l=a(o),s=n(54),u=a(s),d=n(65),c=a(d),m=n(38),g=a(m),p=n(51),f=a(p),b=n(63),v=a(b),y=n(50),q=a(y),h=n(62),$=a(h),T=n(70),k=a(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method",headers:"Headers",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","aws-access-key-id":"AWS Access Key ID","aws-access-key-id-required":"AWS Access Key ID is required","aws-secret-access-key":"AWS Secret Access Key","aws-secret-access-key-required":"AWS Secret Access Key is required","aws-region":"AWS Region","aws-region-required":"AWS Region is required","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","message-attributes":"Message attributes","connect-timeout":"Connection timeout (sec)","connect-timeout-required":"Connection timeout is required.","connect-timeout-range":"Connection timeout should be in a range from 1 to 200.","client-id":"Client ID","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"CA certificate file *","private-key":"Private key file *",cert:"Certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","smtp-protocol":"SMTP protocol","smtp-host":"SMTP host","smtp-host-required":"SMTP host is required.","smtp-port":"SMTP port","smtp-port-required":"You must supply a smtp port.","smtp-port-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout (msec)","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){(0,o.default)(t);for(var n in t){var a=t[n];e.translations(n,a)}}r.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(69),o=a(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES_REQUEST:{name:"Post attributes",value:"POST_ATTRIBUTES_REQUEST"},POST_TELEMETRY_REQUEST:{name:"Post telemetry",value:"POST_TELEMETRY_REQUEST"},TO_SERVER_RPC_REQUEST:{name:"RPC Request",value:"TO_SERVER_RPC_REQUEST"},ACTIVITY_EVENT:{name:"Activity Event",value:"ACTIVITY_EVENT"},INACTIVITY_EVENT:{name:"Inactivity Event",value:"INACTIVITY_EVENT"},CONNECT_EVENT:{name:"Connect Event",value:"CONNECT_EVENT"},DISCONNECT_EVENT:{name:"Disconnect Event",value:"DISCONNECT_EVENT"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}},httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}])); //# sourceMappingURL=rulenode-core-config.js.map \ No newline at end of file diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js index 1b417ddd3c..821d82d778 100644 --- a/ui/src/app/api/rule-chain.service.js +++ b/ui/src/app/api/rule-chain.service.js @@ -34,7 +34,8 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co getRuleNodeComponentByClazz: getRuleNodeComponentByClazz, getRuleNodeSupportedLinks: getRuleNodeSupportedLinks, resolveTargetRuleChains: resolveTargetRuleChains, - testScript: testScript + testScript: testScript, + getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput }; return service; @@ -313,4 +314,15 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co return deferred.promise; } + function getLatestRuleNodeDebugInput(ruleNodeId) { + var deferred = $q.defer(); + var url = '/api/ruleNode/' + ruleNodeId + '/debugIn'; + $http.get(url).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + } diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js index 9bb8c48f11..cf13e96ba2 100644 --- a/ui/src/app/rulechain/rulenode-config.directive.js +++ b/ui/src/app/rulechain/rulenode-config.directive.js @@ -68,6 +68,7 @@ export default function RuleNodeConfigDirective($compile, $templateCache, $injec restrict: "E", require: "^ngModel", scope: { + ruleNodeId:'=', nodeDefinition:'=', required:'=ngRequired', readonly:'=ngReadonly' diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html index 32d5347b43..3148c395f8 100644 --- a/ui/src/app/rulechain/rulenode-config.tpl.html +++ b/ui/src/app/rulechain/rulenode-config.tpl.html @@ -17,6 +17,7 @@ --> `; + var template = `<${directive} rule-node-id="ruleNodeId" ng-model="configuration" ng-required="required" ng-readonly="readonly">`; element.html(template); scope.ruleNodeConfigScope = scope.$new(); $compile(element.contents())(scope.ruleNodeConfigScope); @@ -58,6 +58,7 @@ export default function RuleNodeDefinedConfigDirective($compile) { restrict: "E", require: "^ngModel", scope: { + ruleNodeId:'=', required:'=ngRequired', readonly:'=ngReadonly' }, diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html index 7b0fae5a5f..12ff3202cf 100644 --- a/ui/src/app/rulechain/rulenode-fieldset.tpl.html +++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html @@ -37,6 +37,7 @@ diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js index c685634cf5..81d81c1c54 100644 --- a/ui/src/app/rulechain/script/node-script-test.service.js +++ b/ui/src/app/rulechain/script/node-script-test.service.js @@ -21,7 +21,7 @@ import nodeScriptTestTemplate from './node-script-test.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function NodeScriptTest($q, $mdDialog, $document) { +export default function NodeScriptTest($q, $mdDialog, $document, ruleChainService) { var service = { testNodeScript: testNodeScript @@ -29,12 +29,72 @@ export default function NodeScriptTest($q, $mdDialog, $document) { return service; - function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) { + function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, ruleNodeId) { var deferred = $q.defer(); - if ($event) { $event.stopPropagation(); } + + var msg, metadata, msgType; + if (ruleNodeId) { + ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).then( + (debugIn) => { + if (debugIn) { + if (debugIn.data) { + msg = angular.fromJson(debugIn.data); + } + if (debugIn.metadata) { + metadata = angular.fromJson(debugIn.metadata); + } + msgType = debugIn.msgType; + } + openTestScriptDialog($event, script, scriptType, functionTitle, + functionName, argNames, msg, metadata, msgType).then( + (script) => { + deferred.resolve(script); + }, + () => { + deferred.reject(); + } + ); + }, + () => { + deferred.reject(); + } + ); + } else { + openTestScriptDialog($event, script, scriptType, functionTitle, + functionName, argNames).then( + (script) => { + deferred.resolve(script); + }, + () => { + deferred.reject(); + } + ); + } + return deferred.promise; + } + + function openTestScriptDialog($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) { + var deferred = $q.defer(); + if (!msg) { + msg = { + temperature: 22.4, + humidity: 78 + }; + } + if (!metadata) { + metadata = { + deviceType: "default", + deviceName: "Test Device", + ts: new Date().getTime() + "" + }; + } + if (!msgType) { + msgType = "POST_TELEMETRY_REQUEST"; + } + var onShowingCallback = { onShowed: () => { } @@ -74,7 +134,6 @@ export default function NodeScriptTest($q, $mdDialog, $document) { deferred.reject(); } ); - return deferred.promise; } diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html index 933724061f..42fc8857fc 100644 --- a/ui/src/app/rulechain/script/node-script-test.tpl.html +++ b/ui/src/app/rulechain/script/node-script-test.tpl.html @@ -38,7 +38,7 @@
- +
From 30ba90f581bf8f2f04d3762d99a5275058f16fae Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 14 May 2018 19:36:23 +0300 Subject: [PATCH 5/5] Fixed tests. --- .../thingsboard/rule/engine/filter/TbJsFilterNodeTest.java | 4 ++-- .../rule/engine/transform/TbTransformMsgNodeTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index d0b13ebc2e..50dd86941d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -62,7 +62,7 @@ public class TbJsFilterNodeTest { node.onMsg(ctx, msg); verify(ctx).getJsExecutor(); - verify(ctx).tellNext(msg, "false"); + verify(ctx).tellNext(msg, "False"); } @Test @@ -88,7 +88,7 @@ public class TbJsFilterNodeTest { node.onMsg(ctx, msg); verify(ctx).getJsExecutor(); - verify(ctx).tellNext(msg, "true"); + verify(ctx).tellNext(msg, "True"); } private void initWithScript() throws TbNodeException { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index c488fd314a..5a71b7c08d 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -69,7 +69,7 @@ public class TbTransformMsgNodeTest { node.onMsg(ctx, msg); verify(ctx).getJsExecutor(); ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx).tellNext(captor.capture(), SUCCESS); + verify(ctx).tellNext(captor.capture(), eq(SUCCESS)); TbMsg actualMsg = captor.getValue(); assertEquals(transformedMsg, actualMsg); }