Browse Source

Refactoring and merge with develop/1.5

pull/789/head
Andrew Shvayka 8 years ago
parent
commit
e4f2c8dffa
  1. 19
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 15
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 8
      application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
  4. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 36
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
  6. 1
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleChainMsg.java
  7. 1
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
  8. 4
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  9. 47
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  10. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  11. 2
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  12. 34
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  13. 39
      application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
  15. 354
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  16. 16
      application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
  17. 75
      application/src/main/proto/cluster.proto
  18. 9
      common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  19. 3
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  20. 6
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  21. 5
      dao/src/main/java/org/thingsboard/server/dao/event/BaseEventService.java
  22. 15
      dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
  23. 12
      dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java
  24. 5
      dao/src/main/java/org/thingsboard/server/dao/event/EventService.java
  25. 16
      dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
  26. 11
      dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java
  27. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  28. 14
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
  29. 7
      netty-mqtt/.gitignore
  30. 99
      netty-mqtt/pom.xml
  31. 43
      netty-mqtt/src/main/java/org/thingsboard/mqtt/ChannelClosedException.java
  32. 269
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  33. 205
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
  34. 29
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java
  35. 149
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientConfig.java
  36. 484
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  37. 45
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttConnectResult.java
  38. 23
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java
  39. 48
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttIncomingQos2Publish.java
  40. 154
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttLastWill.java
  41. 101
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java
  42. 102
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java
  43. 55
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java
  44. 98
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPingHandler.java
  45. 84
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java
  46. 66
      netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java
  47. 17
      pom.xml
  48. 8
      rule-engine/rule-engine-components/pom.xml
  49. 19
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/DonAsynchron.java
  50. 117
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  51. 26
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  52. 219
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
  53. 80
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  54. 36
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  55. 106
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  56. 19
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  57. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/DeviceRelationsQuery.java
  58. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  59. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  60. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
  61. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
  62. 103
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  63. 62
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  64. 52
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  65. 48
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
  66. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
  67. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
  68. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  69. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java
  70. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java
  71. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java
  72. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java
  73. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  74. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java
  75. 56
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java
  76. 6
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  77. 98
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
  78. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
  79. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
  80. 4
      ui/src/app/alarm/alarm-table.directive.js
  81. 7
      ui/src/app/alarm/alarm-table.tpl.html
  82. 14
      ui/src/app/api/rule-chain.service.js
  83. 2
      ui/src/app/event/event-header-debug-rulenode.tpl.html
  84. 2
      ui/src/app/event/event-row-debug-rulenode.tpl.html
  85. 4
      ui/src/app/locale/locale.constant.js
  86. 2
      ui/src/app/rulechain/rulechain.controller.js
  87. 13
      ui/src/app/rulechain/rulechain.scss
  88. 11
      ui/src/app/rulechain/rulechain.tpl.html
  89. 1
      ui/src/app/rulechain/rulenode-config.directive.js
  90. 1
      ui/src/app/rulechain/rulenode-config.tpl.html
  91. 3
      ui/src/app/rulechain/rulenode-defined-config.directive.js
  92. 1
      ui/src/app/rulechain/rulenode-fieldset.tpl.html
  93. 67
      ui/src/app/rulechain/script/node-script-test.service.js
  94. 2
      ui/src/app/rulechain/script/node-script-test.tpl.html

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

@ -321,23 +321,23 @@ public class ActorSystemContext {
return discoveryService.getCurrentServer().getServerAddress().toString(); return discoveryService.getCurrentServer().getServerAddress().toString();
} }
public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
persistDebug(tenantId, entityId, "IN", tbMsg, null); persistDebug(tenantId, entityId, "IN", tbMsg, relationType, null);
} }
public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) { public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
persistDebug(tenantId, entityId, "IN", tbMsg, error); persistDebug(tenantId, entityId, "IN", tbMsg, relationType, error);
} }
public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) { public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType, Throwable error) {
persistDebug(tenantId, entityId, "OUT", tbMsg, error); persistDebug(tenantId, entityId, "OUT", tbMsg, relationType, error);
} }
public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) { public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, String relationType) {
persistDebug(tenantId, entityId, "OUT", tbMsg, null); 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 { try {
Event event = new Event(); Event event = new Event();
event.setTenantId(tenantId); event.setTenantId(tenantId);
@ -354,6 +354,7 @@ public class ActorSystemContext {
.put("msgId", tbMsg.getId().toString()) .put("msgId", tbMsg.getId().toString())
.put("msgType", tbMsg.getType()) .put("msgType", tbMsg.getType())
.put("dataType", tbMsg.getDataType().name()) .put("dataType", tbMsg.getDataType().name())
.put("relationType", relationType)
.put("data", tbMsg.getData()) .put("data", tbMsg.getData())
.put("metadata", metadata); .put("metadata", metadata);

15
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -356,16 +356,15 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
for (Map.Entry<Long, List<KvEntry>> entry : tsData.entrySet()) { for (Map.Entry<Long, List<KvEntry>> entry : tsData.entrySet()) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("ts", entry.getKey());
JsonObject values = new JsonObject();
for (KvEntry kv : entry.getValue()) { for (KvEntry kv : entry.getValue()) {
kv.getBooleanValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); kv.getBooleanValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
kv.getLongValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); kv.getLongValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
kv.getDoubleValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); kv.getDoubleValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
kv.getStrValue().ifPresent(v -> values.addProperty(kv.getKey(), v)); kv.getStrValue().ifPresent(v -> json.addProperty(kv.getKey(), v));
} }
json.add("values", values); TbMsgMetaData metaData = defaultMetaData.copy();
TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, defaultMetaData.copy(), TbMsgDataType.JSON, gson.toJson(json), null, null, 0L); 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); pushToRuleEngineWithTimeout(context, tbMsg, msgData);
} }
} }

8
application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java

@ -29,15 +29,12 @@ import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
@Slf4j @Slf4j
public class BasicRpcSessionListener implements GrpcSessionListener { public class BasicRpcSessionListener implements GrpcSessionListener {
public static final String SESSION_RECEIVED_SESSION_ACTOR_MSG = "{} session [{}] received session actor msg {}";
private final ActorSystemContext context;
private final ActorService service; private final ActorService service;
private final ActorRef manager; private final ActorRef manager;
private final ActorRef self; private final ActorRef self;
public BasicRpcSessionListener(ActorSystemContext context, ActorRef manager, ActorRef self) { public BasicRpcSessionListener(ActorService service, ActorRef manager, ActorRef self) {
this.context = context; this.service = service;
this.service = context.getActorService();
this.manager = manager; this.manager = manager;
this.self = self; this.self = self;
} }
@ -64,7 +61,6 @@ public class BasicRpcSessionListener implements GrpcSessionListener {
service.onRecievedMsg(clusterMessage); service.onRecievedMsg(clusterMessage);
} }
@Override @Override
public void onError(GrpcSession session, Throwable t) { public void onError(GrpcSession session, Throwable t) {
log.warn("{} session got error -> {}", getType(session), session.getRemoteServer(), t); log.warn("{} session got error -> {}", getType(session), session.getRemoteServer(), t);

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -69,7 +69,7 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void tellNext(TbMsg msg, String relationType, Throwable th) { public void tellNext(TbMsg msg, String relationType, Throwable th) {
if (nodeCtx.getSelf().isDebugMode()) { 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()); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationType, msg), nodeCtx.getSelfActor());
} }
@ -102,7 +102,7 @@ class DefaultTbContext implements TbContext {
@Override @Override
public void tellError(TbMsg msg, Throwable th) { public void tellError(TbMsg msg, Throwable th) {
if (nodeCtx.getSelf().isDebugMode()) { 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()); nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor());
} }

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

@ -96,12 +96,12 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
private void reprocess(List<RuleNode> ruleNodeList) { private void reprocess(List<RuleNode> ruleNodeList) {
for (RuleNode ruleNode : ruleNodeList) { for (RuleNode ruleNode : ruleNodeList) {
for (TbMsg tbMsg : queue.findUnprocessed(ruleNode.getId().getId(), systemContext.getQueuePartitionId())) { for (TbMsg tbMsg : queue.findUnprocessed(ruleNode.getId().getId(), systemContext.getQueuePartitionId())) {
pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg); pushMsgToNode(nodeActors.get(ruleNode.getId()), tbMsg, "");
} }
} }
if (firstNode != null) { if (firstNode != null) {
for (TbMsg tbMsg : queue.findUnprocessed(entityId.getId(), systemContext.getQueuePartitionId())) { for (TbMsg tbMsg : queue.findUnprocessed(entityId.getId(), systemContext.getQueuePartitionId())) {
pushMsgToNode(firstNode, tbMsg); pushMsgToNode(firstNode, tbMsg, "");
} }
} }
} }
@ -183,13 +183,13 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) { void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) {
checkActive(); checkActive();
putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> pushMsgToNode(firstNode, msg)); putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> pushMsgToNode(firstNode, msg, ""));
} }
void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg envelope) { void onDeviceActorToRuleEngineMsg(DeviceActorToRuleEngineMsg envelope) {
checkActive(); checkActive();
putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> { putToQueue(enrichWithRuleChainId(envelope.getTbMsg()), msg -> {
pushMsgToNode(firstNode, msg); pushMsgToNode(firstNode, msg, "");
envelope.getCallbackRef().tell(new RuleEngineQueuePutAckMsg(msg.getId()), self); envelope.getCallbackRef().tell(new RuleEngineQueuePutAckMsg(msg.getId()), self);
}); });
} }
@ -197,9 +197,9 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) { void onRuleChainToRuleChainMsg(RuleChainToRuleChainMsg envelope) {
checkActive(); checkActive();
if (envelope.isEnqueue()) { if (envelope.isEnqueue()) {
putToQueue(enrichWithRuleChainId(envelope.getMsg()), msg -> pushMsgToNode(firstNode, msg)); putToQueue(enrichWithRuleChainId(envelope.getMsg()), msg -> pushMsgToNode(firstNode, msg, envelope.getFromRelationType()));
} else { } else {
pushMsgToNode(firstNode, envelope.getMsg()); pushMsgToNode(firstNode, envelope.getMsg(), envelope.getFromRelationType());
} }
} }
@ -218,17 +218,17 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
queue.ack(msg, ackId.getId(), msg.getClusterPartition()); queue.ack(msg, ackId.getId(), msg.getClusterPartition());
} else if (relationsCount == 1) { } else if (relationsCount == 1) {
for (RuleNodeRelation relation : relations) { for (RuleNodeRelation relation : relations) {
pushToTarget(msg, relation.getOut()); pushToTarget(msg, relation.getOut(), relation.getType());
} }
} else { } else {
for (RuleNodeRelation relation : relations) { for (RuleNodeRelation relation : relations) {
EntityId target = relation.getOut(); EntityId target = relation.getOut();
switch (target.getEntityType()) { switch (target.getEntityType()) {
case RULE_NODE: case RULE_NODE:
enqueueAndForwardMsgCopyToNode(msg, target); enqueueAndForwardMsgCopyToNode(msg, target, relation.getType());
break; break;
case RULE_CHAIN: case RULE_CHAIN:
enqueueAndForwardMsgCopyToChain(msg, target); enqueueAndForwardMsgCopyToChain(msg, target, relation.getType());
break; break;
} }
} }
@ -237,33 +237,33 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleCh
} }
} }
private void enqueueAndForwardMsgCopyToChain(TbMsg msg, EntityId target) { private void enqueueAndForwardMsgCopyToChain(TbMsg msg, EntityId target, String fromRelationType) {
RuleChainId targetRCId = new RuleChainId(target.getId()); RuleChainId targetRCId = new RuleChainId(target.getId());
TbMsg copyMsg = msg.copy(UUIDs.timeBased(), targetRCId, null, DEFAULT_CLUSTER_PARTITION); TbMsg copyMsg = msg.copy(UUIDs.timeBased(), targetRCId, null, DEFAULT_CLUSTER_PARTITION);
parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, copyMsg, true), self); parent.tell(new RuleChainToRuleChainMsg(new RuleChainId(target.getId()), entityId, copyMsg, fromRelationType, true), self);
} }
private void enqueueAndForwardMsgCopyToNode(TbMsg msg, EntityId target) { private void enqueueAndForwardMsgCopyToNode(TbMsg msg, EntityId target, String fromRelationType) {
RuleNodeId targetId = new RuleNodeId(target.getId()); RuleNodeId targetId = new RuleNodeId(target.getId());
RuleNodeCtx targetNodeCtx = nodeActors.get(targetId); RuleNodeCtx targetNodeCtx = nodeActors.get(targetId);
TbMsg copy = msg.copy(UUIDs.timeBased(), entityId, targetId, DEFAULT_CLUSTER_PARTITION); TbMsg copy = msg.copy(UUIDs.timeBased(), entityId, targetId, DEFAULT_CLUSTER_PARTITION);
putToQueue(copy, queuedMsg -> 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()) { switch (target.getEntityType()) {
case RULE_NODE: case RULE_NODE:
pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg); pushMsgToNode(nodeActors.get(new RuleNodeId(target.getId())), msg, fromRelationType);
break; break;
case RULE_CHAIN: 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; break;
} }
} }
private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg) { private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg, String fromRelationType) {
if (nodeCtx != null) { 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);
} }
} }

1
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 target;
private final RuleChainId source; private final RuleChainId source;
private final TbMsg msg; private final TbMsg msg;
private final String fromRelationType;
private final boolean enqueue; private final boolean enqueue;
@Override @Override

1
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 TbContext ctx;
private final TbMsg msg; private final TbMsg msg;
private final String fromRelationType;
@Override @Override
public MsgType getMsgType() { public MsgType getMsgType() {

4
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -93,7 +93,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception { public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
checkActive(); checkActive();
if (ruleNode.isDebugMode()) { if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg()); systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), "Self");
} }
tbNode.onMsg(defaultCtx, msg.getMsg()); tbNode.onMsg(defaultCtx, msg.getMsg());
} }
@ -101,7 +101,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
checkActive(); checkActive();
if (ruleNode.isDebugMode()) { if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg()); systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
} }
tbNode.onMsg(msg.getCtx(), msg.getMsg()); tbNode.onMsg(msg.getCtx(), msg.getMsg());
} }

47
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -1,12 +1,12 @@
/** /**
* Copyright © 2016-2018 The Thingsboard Authors * Copyright © 2016-2018 The Thingsboard Authors
* * <p>
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* * <p>
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* * <p>
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -28,7 +28,6 @@ import org.thingsboard.server.actors.app.AppActor;
import org.thingsboard.server.actors.rpc.RpcBroadcastMsg; import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
import org.thingsboard.server.actors.rpc.RpcManagerActor; import org.thingsboard.server.actors.rpc.RpcManagerActor;
import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg; import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
import org.thingsboard.server.actors.session.SessionManagerActor; import org.thingsboard.server.actors.session.SessionManagerActor;
import org.thingsboard.server.actors.stats.StatsActor; import org.thingsboard.server.actors.stats.StatsActor;
import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.id.*;
@ -39,14 +38,10 @@ import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg; import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg; import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos; import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
@ -59,10 +54,10 @@ import scala.concurrent.duration.Duration;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.util.Optional;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE_VALUE;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_NETWORK_SERVER_DATA_MESSAGE; import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_NETWORK_SERVER_DATA_MESSAGE;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.RPC_BROADCAST_MSG;
@Service @Service
@Slf4j @Slf4j
@ -157,7 +152,6 @@ public class DefaultActorService implements ActorService {
} }
@Override @Override
public void onServerAdded(ServerInstance server) { public void onServerAdded(ServerInstance server) {
log.trace("Processing onServerAdded msg: {}", server); log.trace("Processing onServerAdded msg: {}", server);
@ -204,8 +198,8 @@ public class DefaultActorService implements ActorService {
rpcService.broadcast(new RpcBroadcastMsg(ClusterAPIProtos.ClusterMessage rpcService.broadcast(new RpcBroadcastMsg(ClusterAPIProtos.ClusterMessage
.newBuilder() .newBuilder()
.setPayload(ByteString .setPayload(ByteString
.copyFrom(actorContext.getEncodingService().encode(msg))) .copyFrom(actorContext.getEncodingService().encode(msg)))
.setMessageType(CLUSTER_NETWORK_SERVER_DATA_MESSAGE) .setMessageType(CLUSTER_ACTOR_MESSAGE)
.build())); .build()));
appActor.tell(msg, ActorRef.noSender()); appActor.tell(msg, ActorRef.noSender());
} }
@ -218,8 +212,9 @@ public class DefaultActorService implements ActorService {
@Override @Override
public void onRecievedMsg(ClusterAPIProtos.ClusterMessage msg) { public void onRecievedMsg(ClusterAPIProtos.ClusterMessage msg) {
switch(msg.getMessageType()) { ServerAddress serverAddress = new ServerAddress(msg.getServerAddress().getHost(), msg.getServerAddress().getPort());
case CLUSTER_NETWORK_SERVER_DATA_MESSAGE: switch (msg.getMessageType()) {
case CLUSTER_ACTOR_MESSAGE:
java.util.Optional<TbActorMsg> decodedMsg = actorContext.getEncodingService() java.util.Optional<TbActorMsg> decodedMsg = actorContext.getEncodingService()
.decode(msg.getPayload().toByteArray()); .decode(msg.getPayload().toByteArray());
if (decodedMsg.isPresent()) { if (decodedMsg.isPresent()) {
@ -229,7 +224,25 @@ public class DefaultActorService implements ActorService {
} }
break; break;
case TO_ALL_NODES_MSG: case TO_ALL_NODES_MSG:
//ToDo //TODO
break;
case CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE:
actorContext.getTsSubService().onNewRemoteSubscription(serverAddress, msg.getPayload().toByteArray());
break;
case CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE:
actorContext.getTsSubService().onRemoteSubscriptionUpdate(serverAddress, msg.getPayload().toByteArray());
break;
case CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE:
actorContext.getTsSubService().onRemoteSubscriptionClose(serverAddress, msg.getPayload().toByteArray());
break;
case CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE:
actorContext.getTsSubService().onRemoteSessionClose(serverAddress, msg.getPayload().toByteArray());
break;
case CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE:
actorContext.getTsSubService().onRemoteAttributesUpdate(serverAddress, msg.getPayload().toByteArray());
break;
case CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE:
actorContext.getTsSubService().onRemoteTsUpdate(serverAddress, msg.getPayload().toByteArray());
break; break;
} }
} }

2
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -93,7 +93,7 @@ public class AlarmController extends BaseController {
try { try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
checkAlarmId(alarmId); checkAlarmId(alarmId);
alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get(); alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get();
} catch (Exception e) { } catch (Exception e) {
throw handleException(e); throw handleException(e);
} }

2
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -607,7 +607,7 @@ public abstract class BaseController {
} }
public static Exception toException(Throwable error) { 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;
} }
} }

34
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.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.thingsboard.rule.engine.api.ScriptEngine; 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.EntityType;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.RuleChainId; 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.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; 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.data.security.Authority;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; 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.dao.model.ModelConstants;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.script.NashornJsEngine; import org.thingsboard.server.service.script.NashornJsEngine;
@ -52,9 +57,13 @@ import java.util.Set;
public class RuleChainController extends BaseController { public class RuleChainController extends BaseController {
public static final String RULE_CHAIN_ID = "ruleChainId"; public static final String RULE_CHAIN_ID = "ruleChainId";
public static final String RULE_NODE_ID = "ruleNodeId";
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private EventService eventService;
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
@ResponseBody @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<Event> 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')") @PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody @ResponseBody

39
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java

@ -1,12 +1,12 @@
/** /**
* Copyright © 2016-2018 The Thingsboard Authors * Copyright © 2016-2018 The Thingsboard Authors
* * <p>
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* * <p>
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* * <p>
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -26,13 +26,18 @@ import org.springframework.util.SerializationUtils;
import org.thingsboard.server.actors.rpc.RpcBroadcastMsg; import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg; import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg; import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos; import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc; import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
import org.thingsboard.server.service.cluster.discovery.ServerInstance; import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.service.cluster.discovery.ServerInstanceService; import org.thingsboard.server.service.cluster.discovery.ServerInstanceService;
import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import java.io.IOException; import java.io.IOException;
@ -52,6 +57,9 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
@Autowired @Autowired
private ServerInstanceService instanceService; private ServerInstanceService instanceService;
@Autowired
private DataDecodingEncodingService encodingService;
private RpcMsgListener listener; private RpcMsgListener listener;
private Server server; private Server server;
@ -77,8 +85,8 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
} }
@Override @Override
public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream) { public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ClusterMessage> inputStream) {
BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = pendingSessionMap.remove(msgUid); BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = pendingSessionMap.remove(msgUid);
if (queue != null) { if (queue != null) {
try { try {
queue.put(inputStream); queue.put(inputStream);
@ -120,7 +128,7 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
listener.onBroadcastMsg(msg); listener.onBroadcastMsg(msg);
} }
private StreamObserver<ClusterAPIProtos.ClusterMessage> createSession(RpcSessionCreateRequestMsg msg) { private StreamObserver<ClusterAPIProtos.ClusterMessage> createSession(RpcSessionCreateRequestMsg msg) {
BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = new ArrayBlockingQueue<>(1); BlockingQueue<StreamObserver<ClusterAPIProtos.ClusterMessage>> queue = new ArrayBlockingQueue<>(1);
pendingSessionMap.put(msg.getMsgUid(), queue); pendingSessionMap.put(msg.getMsgUid(), queue);
listener.onRpcSessionCreateRequestMsg(msg); listener.onRpcSessionCreateRequestMsg(msg);
@ -139,5 +147,22 @@ public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceI
listener.onSendMsg(message); listener.onSendMsg(message);
} }
@Override
public void tell(ServerAddress serverAddress, TbActorMsg actorMsg) {
listener.onSendMsg(encodingService.convertToProtoDataMessage(serverAddress, actorMsg));
}
@Override
public void tell(ServerAddress serverAddress, ClusterAPIProtos.MessageType msgType, byte[] data) {
ClusterAPIProtos.ClusterMessage msg = ClusterAPIProtos.ClusterMessage
.newBuilder()
.setServerAddress(ClusterAPIProtos.ServerAddress
.newBuilder()
.setHost(serverAddress.getHost())
.setPort(serverAddress.getPort())
.build())
.setMessageType(msgType)
.setPayload(ByteString.copyFrom(data)).build();
listener.onSendMsg(msg);
}
} }

2
application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.msg.device.DeviceToDeviceActorMsg;
import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg; import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg; import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg; import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos; import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg;
@ -45,4 +46,5 @@ public interface ClusterRpcService {
void tell(ServerAddress serverAddress, TbActorMsg actorMsg); void tell(ServerAddress serverAddress, TbActorMsg actorMsg);
void tell(ServerAddress serverAddress, ClusterAPIProtos.MessageType msgType, byte[] data);
} }

354
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -1,12 +1,12 @@
/** /**
* Copyright © 2016-2018 The Thingsboard Authors * Copyright © 2016-2018 The Thingsboard Authors
* * <p>
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* * <p>
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* * <p>
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -18,30 +18,40 @@ package org.thingsboard.server.service.telemetry;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.DonAsynchron;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature; import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService; import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService; import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.state.DefaultDeviceStateService; import org.thingsboard.server.service.state.DefaultDeviceStateService;
@ -54,15 +64,18 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
/** /**
* Created by ashvayka on 27.03.18. * Created by ashvayka on 27.03.18.
@ -120,8 +133,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
ServerAddress address = server.get(); ServerAddress address = server.get();
log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address); log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
subscription = new Subscription(sub, true, address); subscription = new Subscription(sub, true, address);
// rpcService.tell(); tellNewSubscription(address, sessionId, subscription);
// rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
} else { } else {
log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId); log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
subscription = new Subscription(sub, true); subscription = new Subscription(sub, true);
@ -193,6 +205,174 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
, System.currentTimeMillis())), callback); , System.currentTimeMillis())), callback);
} }
@Override
public void onNewRemoteSubscription(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.SubscriptionProto proto;
try {
proto = ClusterAPIProtos.SubscriptionProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(
Collectors.toMap(ClusterAPIProtos.SubscriptionKetStateProto::getKey, ClusterAPIProtos.SubscriptionKetStateProto::getTs));
Subscription subscription = new Subscription(
new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(),
EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()),
TelemetryFeature.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
false, new ServerAddress(serverAddress.getHost(), serverAddress.getPort()));
addRemoteWsSubscription(serverAddress, proto.getSessionId(), subscription);
}
@Override
public void onRemoteSubscriptionUpdate(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.SubscriptionUpdateProto proto;
try {
proto = ClusterAPIProtos.SubscriptionUpdateProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
SubscriptionUpdate update = convert(proto);
String sessionId = proto.getSessionId();
log.trace("[{}] Processing remote subscription onUpdate [{}]", sessionId, update);
Optional<Subscription> subOpt = getSubscription(sessionId, update.getSubscriptionId());
if (subOpt.isPresent()) {
updateSubscriptionState(sessionId, subOpt.get(), update);
wsService.sendWsMsg(sessionId, update);
}
}
@Override
public void onRemoteSubscriptionClose(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.SubscriptionCloseProto proto;
try {
proto = ClusterAPIProtos.SubscriptionCloseProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
removeSubscription(proto.getSessionId(), proto.getSubscriptionId());
}
@Override
public void onRemoteSessionClose(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.SessionCloseProto proto;
try {
proto = ClusterAPIProtos.SessionCloseProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
cleanupRemoteWsSessionSubscriptions(proto.getSessionId());
}
@Override
public void onRemoteAttributesUpdate(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.AttributeUpdateProto proto;
try {
proto = ClusterAPIProtos.AttributeUpdateProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
onAttributesUpdate(EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), proto.getScope(),
proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList()));
}
@Override
public void onRemoteTsUpdate(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.TimeseriesUpdateProto proto;
try {
proto = ClusterAPIProtos.TimeseriesUpdateProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
onTimeseriesUpdate(EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()),
proto.getDataList().stream().map(this::toTimeseries).collect(Collectors.toList()));
}
@Override
public void onClusterUpdate() {
log.trace("Processing cluster onUpdate msg!");
Iterator<Map.Entry<EntityId, Set<Subscription>>> deviceIterator = subscriptionsByEntityId.entrySet().iterator();
while (deviceIterator.hasNext()) {
Map.Entry<EntityId, Set<Subscription>> e = deviceIterator.next();
Set<Subscription> subscriptions = e.getValue();
Optional<ServerAddress> newAddressOptional = routingService.resolveById(e.getKey());
if (newAddressOptional.isPresent()) {
newAddressOptional.ifPresent(serverAddress -> checkSubsciptionsNewAddress(serverAddress, subscriptions));
} else {
checkSubsciptionsPrevAddress(subscriptions);
}
if (subscriptions.size() == 0) {
log.trace("[{}] No more subscriptions for this device on current server.", e.getKey());
deviceIterator.remove();
}
}
}
private void checkSubsciptionsNewAddress(ServerAddress newAddress, Set<Subscription> subscriptions) {
Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
while (subscriptionIterator.hasNext()) {
Subscription s = subscriptionIterator.next();
if (s.isLocal()) {
if (!newAddress.equals(s.getServer())) {
log.trace("[{}] Local subscription is now handled on new server [{}]", s.getWsSessionId(), newAddress);
s.setServer(newAddress);
tellNewSubscription(newAddress, s.getWsSessionId(), s);
}
} else {
log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress);
subscriptionIterator.remove();
//TODO: onUpdate state of subscription by WsSessionId and other maps.
}
}
}
private void checkSubsciptionsPrevAddress(Set<Subscription> subscriptions) {
for (Subscription s : subscriptions) {
if (s.isLocal() && s.getServer() != null) {
log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer());
s.setServer(null);
} else {
log.trace("[{}] Remote subscription is on up to date server address.", s.getWsSessionId());
}
}
}
private void addRemoteWsSubscription(ServerAddress address, String sessionId, Subscription subscription) {
EntityId entityId = subscription.getEntityId();
log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address);
registerSubscription(sessionId, entityId, subscription);
if (subscription.getType() == TelemetryFeature.ATTRIBUTES) {
final Map<String, Long> keyStates = subscription.getKeyStates();
DonAsynchron.withCallback(attrService.find(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet()), values -> {
List<TsKvEntry> missedUpdates = new ArrayList<>();
values.forEach(latestEntry -> {
if (latestEntry.getLastUpdateTs() > keyStates.get(latestEntry.getKey())) {
missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
}
});
if (!missedUpdates.isEmpty()) {
tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
}
},
e -> log.error("Failed to fetch missed updates.", e), tsCallBackExecutor);
} else if (subscription.getType() == TelemetryFeature.TIMESERIES) {
long curTs = System.currentTimeMillis();
List<TsKvQuery> queries = new ArrayList<>();
subscription.getKeyStates().entrySet().forEach(e -> {
queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs));
});
DonAsynchron.withCallback(tsService.findAll(entityId, queries),
missedUpdates -> {
if (!missedUpdates.isEmpty()) {
tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
}
},
e -> log.error("Failed to fetch missed updates.", e),
tsCallBackExecutor);
}
}
private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) { private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
Optional<ServerAddress> serverAddress = routingService.resolveById(entityId); Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
if (!serverAddress.isPresent()) { if (!serverAddress.isPresent()) {
@ -205,7 +385,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
} }
} }
} else { } else {
// rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, entries); tellRemoteAttributesUpdate(serverAddress.get(), entityId, scope, attributes);
} }
} }
@ -214,7 +394,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
if (!serverAddress.isPresent()) { if (!serverAddress.isPresent()) {
onLocalTimeseriesUpdate(entityId, ts); onLocalTimeseriesUpdate(entityId, ts);
} else { } else {
// rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries); tellRemoteTimeseriesUpdate(serverAddress.get(), entityId, ts);
} }
} }
@ -260,8 +440,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
updateSubscriptionState(sessionId, s, update); updateSubscriptionState(sessionId, s, update);
wsService.sendWsMsg(sessionId, update); wsService.sendWsMsg(sessionId, update);
} else { } else {
//TODO: ashvayka tellRemoteSubUpdate(s.getServer(), sessionId, update);
// rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
} }
} }
}); });
@ -282,11 +461,11 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
} }
public void cleanupLocalWsSessionSubscriptions(String sessionId) { private void cleanupLocalWsSessionSubscriptions(String sessionId) {
cleanupWsSessionSubscriptions(sessionId, true); cleanupWsSessionSubscriptions(sessionId, true);
} }
public void cleanupRemoteWsSessionSubscriptions(String sessionId) { private void cleanupRemoteWsSessionSubscriptions(String sessionId) {
cleanupWsSessionSubscriptions(sessionId, false); cleanupWsSessionSubscriptions(sessionId, false);
} }
@ -324,14 +503,14 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
} }
for (ServerAddress address : affectedServers) { for (ServerAddress address : affectedServers) {
log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address); log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
// rpcHandler.onSessionClose(ctx, address, sessionId); tellRemoteSessionClose(address, sessionId);
} }
} }
private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) { private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) {
EntityId entityId = subscription.getEntityId(); EntityId entityId = subscription.getEntityId();
if (subscription.isLocal() && subscription.getServer() != null) { if (subscription.isLocal() && subscription.getServer() != null) {
// rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId()); tellRemoteSubClose(subscription.getServer(), sessionId, subscription.getSubscriptionId());
} }
if (sessionSubscriptions.isEmpty()) { if (sessionSubscriptions.isEmpty()) {
log.debug("[{}] Removed last subscription for particular session.", sessionId); log.debug("[{}] Removed last subscription for particular session.", sessionId);
@ -383,4 +562,151 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
} }
}, wsCallBackExecutor); }, wsCallBackExecutor);
} }
private void tellNewSubscription(ServerAddress address, String sessionId, Subscription sub) {
ClusterAPIProtos.SubscriptionProto.Builder builder = ClusterAPIProtos.SubscriptionProto.newBuilder();
builder.setSessionId(sessionId);
builder.setSubscriptionId(sub.getSubscriptionId());
builder.setEntityType(sub.getEntityId().getEntityType().name());
builder.setEntityId(sub.getEntityId().getId().toString());
builder.setType(sub.getType().name());
builder.setAllKeys(sub.isAllKeys());
builder.setScope(sub.getScope());
sub.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(
ClusterAPIProtos.SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE, builder.build().toByteArray());
}
private void tellRemoteSubUpdate(ServerAddress address, String sessionId, SubscriptionUpdate update) {
ClusterAPIProtos.SubscriptionUpdateProto.Builder builder = ClusterAPIProtos.SubscriptionUpdateProto.newBuilder();
builder.setSessionId(sessionId);
builder.setSubscriptionId(update.getSubscriptionId());
builder.setErrorCode(update.getErrorCode());
if (update.getErrorMsg() != null) {
builder.setErrorMsg(update.getErrorMsg());
}
update.getData().entrySet().forEach(
e -> {
ClusterAPIProtos.SubscriptionUpdateValueListProto.Builder dataBuilder = ClusterAPIProtos.SubscriptionUpdateValueListProto.newBuilder();
dataBuilder.setKey(e.getKey());
e.getValue().forEach(v -> {
Object[] array = (Object[]) v;
dataBuilder.addTs((long) array[0]);
dataBuilder.addValue((String) array[1]);
});
builder.addData(dataBuilder.build());
}
);
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE, builder.build().toByteArray());
}
private void tellRemoteAttributesUpdate(ServerAddress address, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
ClusterAPIProtos.AttributeUpdateProto.Builder builder = ClusterAPIProtos.AttributeUpdateProto.newBuilder();
builder.setEntityId(entityId.getId().toString());
builder.setEntityType(entityId.getEntityType().name());
builder.setScope(scope);
attributes.forEach(v -> builder.addData(toKeyValueProto(v.getLastUpdateTs(), v).build()));
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE, builder.build().toByteArray());
}
private void tellRemoteTimeseriesUpdate(ServerAddress address, EntityId entityId, List<TsKvEntry> ts) {
ClusterAPIProtos.TimeseriesUpdateProto.Builder builder = ClusterAPIProtos.TimeseriesUpdateProto.newBuilder();
builder.setEntityId(entityId.getId().toString());
builder.setEntityType(entityId.getEntityType().name());
ts.forEach(v -> builder.addData(toKeyValueProto(v.getTs(), v).build()));
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE, builder.build().toByteArray());
}
private void tellRemoteSessionClose(ServerAddress address, String sessionId) {
ClusterAPIProtos.SessionCloseProto proto = ClusterAPIProtos.SessionCloseProto.newBuilder().setSessionId(sessionId).build();
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE, proto.toByteArray());
}
private void tellRemoteSubClose(ServerAddress address, String sessionId, int subscriptionId) {
ClusterAPIProtos.SubscriptionCloseProto proto = ClusterAPIProtos.SubscriptionCloseProto.newBuilder().setSessionId(sessionId).setSubscriptionId(subscriptionId).build();
rpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE, proto.toByteArray());
}
private ClusterAPIProtos.KeyValueProto.Builder toKeyValueProto(long ts, KvEntry attr) {
ClusterAPIProtos.KeyValueProto.Builder dataBuilder = ClusterAPIProtos.KeyValueProto.newBuilder();
dataBuilder.setKey(attr.getKey());
dataBuilder.setTs(ts);
dataBuilder.setValueType(attr.getDataType().ordinal());
switch (attr.getDataType()) {
case BOOLEAN:
Optional<Boolean> booleanValue = attr.getBooleanValue();
booleanValue.ifPresent(dataBuilder::setBoolValue);
break;
case LONG:
Optional<Long> longValue = attr.getLongValue();
longValue.ifPresent(dataBuilder::setLongValue);
break;
case DOUBLE:
Optional<Double> doubleValue = attr.getDoubleValue();
doubleValue.ifPresent(dataBuilder::setDoubleValue);
break;
case STRING:
Optional<String> stringValue = attr.getStrValue();
stringValue.ifPresent(dataBuilder::setStrValue);
break;
}
return dataBuilder;
}
private AttributeKvEntry toAttribute(ClusterAPIProtos.KeyValueProto proto) {
return new BaseAttributeKvEntry(getKvEntry(proto), proto.getTs());
}
private TsKvEntry toTimeseries(ClusterAPIProtos.KeyValueProto proto) {
return new BasicTsKvEntry(proto.getTs(), getKvEntry(proto));
}
private KvEntry getKvEntry(ClusterAPIProtos.KeyValueProto proto) {
KvEntry entry = null;
DataType type = DataType.values()[proto.getValueType()];
switch (type) {
case BOOLEAN:
entry = new BooleanDataEntry(proto.getKey(), proto.getBoolValue());
break;
case LONG:
entry = new LongDataEntry(proto.getKey(), proto.getLongValue());
break;
case DOUBLE:
entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleValue());
break;
case STRING:
entry = new StringDataEntry(proto.getKey(), proto.getStrValue());
break;
}
return entry;
}
private SubscriptionUpdate convert(ClusterAPIProtos.SubscriptionUpdateProto proto) {
if (proto.getErrorCode() > 0) {
return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
} else {
Map<String, List<Object>> data = new TreeMap<>();
proto.getDataList().forEach(v -> {
List<Object> values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>());
for (int i = 0; i < v.getTsCount(); i++) {
Object[] value = new Object[2];
value[0] = v.getTs(i);
value[1] = v.getValue(i);
values.add(value);
}
});
return new SubscriptionUpdate(proto.getSubscriptionId(), data);
}
}
private Optional<Subscription> getSubscription(String sessionId, int subscriptionId) {
Subscription state = null;
Map<Integer, Subscription> subMap = subscriptionsByWsSessionId.get(sessionId);
if (subMap != null) {
state = subMap.get(subscriptionId);
}
return Optional.ofNullable(state);
}
} }

16
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java

@ -17,7 +17,10 @@ package org.thingsboard.server.service.telemetry;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
/** /**
* Created by ashvayka on 27.03.18. * Created by ashvayka on 27.03.18.
@ -30,4 +33,17 @@ public interface TelemetrySubscriptionService extends RuleEngineTelemetryService
void removeSubscription(String sessionId, int cmdId); void removeSubscription(String sessionId, int cmdId);
void onNewRemoteSubscription(ServerAddress serverAddress, byte[] data);
void onRemoteSubscriptionUpdate(ServerAddress serverAddress, byte[] bytes);
void onRemoteSubscriptionClose(ServerAddress serverAddress, byte[] bytes);
void onRemoteSessionClose(ServerAddress serverAddress, byte[] bytes);
void onRemoteAttributesUpdate(ServerAddress serverAddress, byte[] bytes);
void onRemoteTsUpdate(ServerAddress serverAddress, byte[] bytes);
void onClusterUpdate();
} }

75
application/src/main/proto/cluster.proto

@ -25,7 +25,7 @@ service ClusterRpcService {
message ClusterMessage { message ClusterMessage {
MessageType messageType = 1; MessageType messageType = 1;
MessageMataInfo messageMetaInfo = 2; MessageMataInfo messageMetaInfo = 2;
ServerAddress serverAdresss = 3; ServerAddress serverAddress = 3;
bytes payload = 4; bytes payload = 4;
} }
@ -49,6 +49,75 @@ enum MessageType {
CONNECT_RPC_MESSAGE =4; CONNECT_RPC_MESSAGE =4;
CLUSTER_ACTOR_MESSAGE = 5; CLUSTER_ACTOR_MESSAGE = 5;
CLUSTER_TELEMETRY_MESSAGE = 6; // Messages related to TelemetrySubscriptionService
CLUSTER_DEVICE_RPC_MESSAGE = 7; CLUSTER_TELEMETRY_SUBSCRIPTION_CREATE_MESSAGE = 6;
CLUSTER_TELEMETRY_SUBSCRIPTION_UPDATE_MESSAGE = 7;
CLUSTER_TELEMETRY_SUBSCRIPTION_CLOSE_MESSAGE = 8;
CLUSTER_TELEMETRY_SESSION_CLOSE_MESSAGE = 9;
CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE = 10;
CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE = 11;
} }
// Messages related to CLUSTER_TELEMETRY_MESSAGE
message SubscriptionProto {
string sessionId = 1;
int32 subscriptionId = 2;
string entityType = 3;
string entityId = 4;
string type = 5;
bool allKeys = 6;
repeated SubscriptionKetStateProto keyStates = 7;
string scope = 8;
}
message SubscriptionUpdateProto {
string sessionId = 1;
int32 subscriptionId = 2;
int32 errorCode = 3;
string errorMsg = 4;
repeated SubscriptionUpdateValueListProto data = 5;
}
message AttributeUpdateProto {
string entityType = 1;
string entityId = 2;
string scope = 3;
repeated KeyValueProto data = 4;
}
message TimeseriesUpdateProto {
string entityType = 1;
string entityId = 2;
repeated KeyValueProto data = 4;
}
message SessionCloseProto {
string sessionId = 1;
}
message SubscriptionCloseProto {
string sessionId = 1;
int32 subscriptionId = 2;
}
message SubscriptionKetStateProto {
string key = 1;
int64 ts = 2;
}
message SubscriptionUpdateValueListProto {
string key = 1;
repeated int64 ts = 2;
repeated string value = 3;
}
message KeyValueProto {
string key = 1;
int64 ts = 2;
int32 valueType = 3;
string strValue = 4;
int64 longValue = 5;
double doubleValue = 6;
bool boolValue = 7;
}

9
common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -36,9 +36,16 @@ public class JsonConverter {
return convertToTelemetry(jsonObject, BasicRequest.DEFAULT_REQUEST_ID); return convertToTelemetry(jsonObject, BasicRequest.DEFAULT_REQUEST_ID);
} }
public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, long ts) throws JsonSyntaxException {
return convertToTelemetry(jsonObject, ts, BasicRequest.DEFAULT_REQUEST_ID);
}
public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, int requestId) throws JsonSyntaxException { public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, int requestId) throws JsonSyntaxException {
return convertToTelemetry(jsonObject, System.currentTimeMillis(), requestId);
}
private static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, long systemTs, int requestId) throws JsonSyntaxException {
BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId); BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId);
long systemTs = System.currentTimeMillis();
if (jsonObject.isJsonObject()) { if (jsonObject.isJsonObject()) {
parseObject(request, systemTs, jsonObject); parseObject(request, systemTs, jsonObject);
} else if (jsonObject.isJsonArray()) { } else if (jsonObject.isJsonArray()) {

3
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.alarm; package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.*; import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -30,7 +31,7 @@ public interface AlarmService {
ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs); ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs); ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long ackTs);
ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId); ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);

6
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.alarm; package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -187,7 +188,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
} }
@Override @Override
public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long clearTime) { public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long clearTime) {
return getAndUpdate(alarmId, new Function<Alarm, Boolean>() { return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
@Nullable @Nullable
@Override @Override
@ -199,6 +200,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
alarm.setStatus(newStatus); alarm.setStatus(newStatus);
alarm.setClearTs(clearTime); alarm.setClearTs(clearTime);
if (details != null) {
alarm.setDetails(details);
}
alarmDao.save(alarm); alarmDao.save(alarm);
updateRelations(alarm, oldStatus, newStatus); updateRelations(alarm, oldStatus, newStatus);
return true; return true;

5
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); return new TimePageData<>(events, pageLink);
} }
@Override
public List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) {
return eventDao.findLatestEvents(tenantId.getId(), entityId, eventType, limit);
}
private DataValidator<Event> eventValidator = private DataValidator<Event> eventValidator =
new DataValidator<Event>() { new DataValidator<Event>() {
@Override @Override

15
dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java

@ -134,6 +134,21 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
return DaoUtil.convertDataList(entities); return DaoUtil.convertDataList(entities);
} }
@Override
public List<Event> 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<EventEntity> entities = findListByStatement(query);
return DaoUtil.convertDataList(entities);
}
private Optional<Event> save(EventEntity entity, boolean ifNotExists) { private Optional<Event> save(EventEntity entity, boolean ifNotExists) {
if (entity.getId() == null) { if (entity.getId() == null) {
entity.setId(UUIDs.timeBased()); entity.setId(UUIDs.timeBased());

12
dao/src/main/java/org/thingsboard/server/dao/event/EventDao.java

@ -76,4 +76,16 @@ public interface EventDao extends Dao<Event> {
* @return the event list * @return the event list
*/ */
List<Event> findEvents(UUID tenantId, EntityId entityId, String eventType, TimePageLink pageLink); List<Event> 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<Event> findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit);
} }

5
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.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
public interface EventService { public interface EventService {
@ -34,4 +36,7 @@ public interface EventService {
TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink); TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, TimePageLink pageLink);
TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink); TimePageData<Event> findEvents(TenantId tenantId, EntityId entityId, String eventType, TimePageLink pageLink);
List<Event> findLatestEvents(TenantId tenantId, EntityId entityId, String eventType, int limit);
} }

16
dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java

@ -15,12 +15,18 @@
*/ */
package org.thingsboard.server.dao.sql.event; 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.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.EntityType; 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.model.sql.EventEntity;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import java.util.List;
/** /**
* Created by Valerii Sosliuk on 5/3/2017. * Created by Valerii Sosliuk on 5/3/2017.
*/ */
@ -36,4 +42,14 @@ public interface EventRepository extends CrudRepository<EventEntity, String>, Jp
EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId, EventEntity findByTenantIdAndEntityTypeAndEntityId(String tenantId,
EntityType entityType, EntityType entityType,
String entityId); 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<EventEntity> findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType(
@Param("tenantId") String tenantId,
@Param("entityType") EntityType entityType,
@Param("entityId") String entityId,
@Param("eventType") String eventType,
Pageable pageable);
} }

11
dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java

@ -109,6 +109,17 @@ public class JpaBaseEventDao extends JpaAbstractSearchTimeDao<EventEntity, Event
return DaoUtil.convertDataList(eventRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent()); return DaoUtil.convertDataList(eventRepository.findAll(where(timeSearchSpec).and(fieldsSpec), pageable).getContent());
} }
@Override
public List<Event> findLatestEvents(UUID tenantId, EntityId entityId, String eventType, int limit) {
List<EventEntity> latest = eventRepository.findLatestByTenantIdAndEntityTypeAndEntityIdAndEventType(
UUIDConverter.fromTimeUUID(tenantId),
entityId.getEntityType(),
UUIDConverter.fromTimeUUID(entityId.getId()),
eventType,
new PageRequest(0, limit));
return DaoUtil.convertDataList(latest);
}
public Optional<Event> save(EventEntity entity, boolean ifNotExists) { public Optional<Event> save(EventEntity entity, boolean ifNotExists) {
log.debug("Save event [{}] ", entity); log.debug("Save event [{}] ", entity);
if (entity.getTenantId() == null) { if (entity.getTenantId() == null) {

2
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.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size()); 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(); created = alarmService.findAlarmByIdAsync(created.getId()).get();
alarms = alarmService.findAlarms(AlarmQuery.builder() alarms = alarmService.findAlarms(AlarmQuery.builder()

14
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java

@ -31,12 +31,14 @@ public class SubscriptionUpdate {
super(); super();
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.data = new TreeMap<>(); this.data = new TreeMap<>();
for (TsKvEntry tsEntry : data) { if (data != null) {
List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); for (TsKvEntry tsEntry : data) {
Object[] value = new Object[2]; List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>());
value[0] = tsEntry.getTs(); Object[] value = new Object[2];
value[1] = tsEntry.getValueAsString(); value[0] = tsEntry.getTs();
values.add(value); value[1] = tsEntry.getValueAsString();
values.add(value);
}
} }
} }

7
netty-mqtt/.gitignore

@ -0,0 +1,7 @@
.idea/
*.ipr
*.iws
*.ids
*.iml
logs
target

99
netty-mqtt/pom.xml

@ -0,0 +1,99 @@
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.4.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId>
<version>1.4.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Netty MQTT Client</name>
<url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/..</main.dir>
</properties>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-mqtt</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>jk-5-maven</id>
<name>jk-5's maven server</name>
<url>sftp://10.2.1.2/opt/maven</url>
</repository>
</distributionManagement>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.6</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

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

269
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<MqttMessage> {
private final MqttClientImpl client;
private final Promise<MqttConnectResult> connectFuture;
MqttChannelHandler(MqttClientImpl client, Promise<MqttConnectResult> 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<MqttSubscribtion> 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();
}
}

205
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<MqttConnectResult> 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<MqttConnectResult> 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<MqttConnectResult> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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);
}

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

149
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<? extends Channel> 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<? extends Channel> getChannelClass() {
return channelClass;
}
public void setChannelClass(Class<? extends Channel> channelClass) {
this.channelClass = channelClass;
}
public SslContext getSslContext() {
return sslContext;
}
public boolean isReconnect() {
return reconnect;
}
public void setReconnect(boolean reconnect) {
this.reconnect = reconnect;
}
}

484
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<String> serverSubscribtions = new HashSet<>();
private final IntObjectHashMap<MqttPendingUnsubscribtion> pendingServerUnsubscribes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttPendingPublish> pendingPublishes = new IntObjectHashMap<>();
private final HashMultimap<String, MqttSubscribtion> subscriptions = HashMultimap.create();
private final IntObjectHashMap<MqttPendingSubscribtion> pendingSubscribtions = new IntObjectHashMap<>();
private final Set<String> pendingSubscribeTopics = new HashSet<>();
private final HashMultimap<MqttHandler, MqttSubscribtion> 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<MqttConnectResult> 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<MqttConnectResult> connect(String host, int port) {
if (this.eventLoop == null) {
this.eventLoop = new NioEventLoopGroup();
}
this.host = host;
this.port = port;
Promise<MqttConnectResult> 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<MqttConnectResult> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> off(String topic, MqttHandler handler) {
Promise<Void> 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<Void> off(String topic) {
Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
ImmutableSet<MqttSubscribtion> 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<Void> 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<Void> 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<Void> 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<Void> publish(String topic, ByteBuf payload, MqttQoS qos, boolean retain) {
Promise<Void> 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<Void> createSubscribtion(String topic, MqttHandler handler, boolean once, MqttQoS qos) {
if (this.pendingSubscribeTopics.contains(topic)) {
Optional<Map.Entry<Integer, MqttPendingSubscribtion>> 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<Void> 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<Void> 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<MqttPendingSubscribtion> getPendingSubscribtions() {
return pendingSubscribtions;
}
HashMultimap<String, MqttSubscribtion> getSubscriptions() {
return subscriptions;
}
Set<String> getPendingSubscribeTopics() {
return pendingSubscribeTopics;
}
HashMultimap<MqttHandler, MqttSubscribtion> getHandlerToSubscribtion() {
return handlerToSubscribtion;
}
Set<String> getServerSubscribtions() {
return serverSubscribtions;
}
IntObjectHashMap<MqttPendingUnsubscribtion> getPendingServerUnsubscribes() {
return pendingServerUnsubscribes;
}
IntObjectHashMap<MqttPendingPublish> getPendingPublishes() {
return pendingPublishes;
}
IntObjectHashMap<MqttIncomingQos2Publish> getQos2PendingIncomingPublishes() {
return qos2PendingIncomingPublishes;
}
private class MqttChannelInitializer extends ChannelInitializer<SocketChannel> {
private final Promise<MqttConnectResult> connectFuture;
private final String host;
private final int port;
private final SslContext sslContext;
public MqttChannelInitializer(Promise<MqttConnectResult> 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));
}
}
}

45
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;
}
}

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

48
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<MqttMessage> retransmissionHandler = new RetransmissionHandler<>();
MqttIncomingQos2Publish(MqttPublishMessage incomingPublish, MqttMessage originalMessage) {
this.incomingPublish = incomingPublish;
this.retransmissionHandler.setOriginalMessage(originalMessage);
}
MqttPublishMessage getIncomingPublish() {
return incomingPublish;
}
void startPubrecRetransmitTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader())));
this.retransmissionHandler.start(eventLoop);
}
void onPubrelReceived() {
this.retransmissionHandler.stop();
}
}

154
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();
}
}

101
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<Void> future;
private final ByteBuf payload;
private final MqttPublishMessage message;
private final MqttQoS qos;
private final RetransmissionHandler<MqttPublishMessage> publishRetransmissionHandler = new RetransmissionHandler<>();
private final RetransmissionHandler<MqttMessage> pubrelRetransmissionHandler = new RetransmissionHandler<>();
private boolean sent = false;
MqttPendingPublish(int messageId, Promise<Void> 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<Void> 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<Object> 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<Object> sendPacket) {
this.pubrelRetransmissionHandler.setHandle((fixedHeader, originalMessage) ->
sendPacket.accept(new MqttMessage(fixedHeader, originalMessage.variableHeader())));
this.pubrelRetransmissionHandler.start(eventLoop);
}
void onPubcompReceived() {
this.pubrelRetransmissionHandler.stop();
}
}

102
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<Void> future;
private final String topic;
private final Set<MqttPendingHandler> handlers = new HashSet<>();
private final MqttSubscribeMessage subscribeMessage;
private final RetransmissionHandler<MqttSubscribeMessage> retransmissionHandler = new RetransmissionHandler<>();
private boolean sent = false;
MqttPendingSubscribtion(Promise<Void> future, String topic, MqttSubscribeMessage message) {
this.future = future;
this.topic = topic;
this.subscribeMessage = message;
this.retransmissionHandler.setOriginalMessage(message);
}
Promise<Void> 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<MqttPendingHandler> getHandlers() {
return handlers;
}
void startRetransmitTimer(EventLoop eventLoop, Consumer<Object> 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;
}
}
}

55
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<Void> future;
private final String topic;
private final RetransmissionHandler<MqttUnsubscribeMessage> retransmissionHandler = new RetransmissionHandler<>();
MqttPendingUnsubscribtion(Promise<Void> future, String topic, MqttUnsubscribeMessage unsubscribeMessage) {
this.future = future;
this.topic = topic;
this.retransmissionHandler.setOriginalMessage(unsubscribeMessage);
}
Promise<Void> getFuture() {
return future;
}
String getTopic() {
return topic;
}
void startRetransmissionTimer(EventLoop eventLoop, Consumer<Object> sendPacket) {
this.retransmissionHandler.setHandle((fixedHeader, originalMessage) ->
sendPacket.accept(new MqttUnsubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload())));
this.retransmissionHandler.start(eventLoop);
}
void onUnsubackReceived(){
this.retransmissionHandler.stop();
}
}

98
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;
}
}
}

84
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;
}
}

66
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<T extends MqttMessage> {
private ScheduledFuture<?> timer;
private int timeout = 10;
private BiConsumer<MqttFixedHeader, T> 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<MqttFixedHeader, T> runnable) {
this.handler = runnable;
}
void setOriginalMessage(T originalMessage) {
this.originalMessage = originalMessage;
}
}

17
pom.xml

@ -79,7 +79,6 @@
<dbunit.version>2.5.3</dbunit.version> <dbunit.version>2.5.3</dbunit.version>
<spring-test-dbunit.version>1.2.1</spring-test-dbunit.version> <spring-test-dbunit.version>1.2.1</spring-test-dbunit.version>
<postgresql.driver.version>9.4.1211</postgresql.driver.version> <postgresql.driver.version>9.4.1211</postgresql.driver.version>
<netty-mqtt-client.version>2.0.0TB</netty-mqtt-client.version>
<sonar.exclusions>org/thingsboard/server/gen/**/*, <sonar.exclusions>org/thingsboard/server/gen/**/*,
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
</sonar.exclusions> </sonar.exclusions>
@ -87,6 +86,7 @@
</properties> </properties>
<modules> <modules>
<module>netty-mqtt</module>
<module>common</module> <module>common</module>
<module>rule-engine</module> <module>rule-engine</module>
<module>dao</module> <module>dao</module>
@ -324,6 +324,11 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>extensions-api</artifactId> <artifactId>extensions-api</artifactId>
@ -568,6 +573,11 @@
<artifactId>netty-handler</artifactId> <artifactId>netty-handler</artifactId>
<version>${netty.version}</version> <version>${netty.version}</version>
</dependency> </dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-mqtt</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.datastax.cassandra</groupId> <groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId> <artifactId>cassandra-driver-core</artifactId>
@ -819,11 +829,6 @@
<type>exe</type> <type>exe</type>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>nl.jk5.netty-mqtt</groupId>
<artifactId>netty-mqtt</artifactId>
<version>${netty-mqtt-client.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.elasticsearch.client</groupId> <groupId>org.elasticsearch.client</groupId>
<artifactId>rest</artifactId> <artifactId>rest</artifactId>

8
rule-engine/rule-engine-components/pom.xml

@ -68,6 +68,10 @@
<groupId>org.thingsboard.rule-engine</groupId> <groupId>org.thingsboard.rule-engine</groupId>
<artifactId>rule-engine-api</artifactId> <artifactId>rule-engine-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
@ -90,10 +94,6 @@
<groupId>com.rabbitmq</groupId> <groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId> <artifactId>amqp-client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>nl.jk5.netty-mqtt</groupId>
<artifactId>netty-mqtt</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId> <artifactId>bcpkix-jdk15on</artifactId>

19
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 com.google.common.util.concurrent.ListenableFuture;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.Executor;
import java.util.function.Consumer; import java.util.function.Consumer;
public class DonAsynchron { public class DonAsynchron {
public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess, Consumer<Throwable> onFailure) { public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess,
Futures.addCallback(future, new FutureCallback<T>() { Consumer<Throwable> onFailure) {
withCallback(future, onSuccess, onFailure, null);
}
public static <T> void withCallback(ListenableFuture<T> future, Consumer<T> onSuccess,
Consumer<Throwable> onFailure, Executor executor) {
FutureCallback<T> callback = new FutureCallback<T>() {
@Override @Override
public void onSuccess(@Nullable T result) { public void onSuccess(@Nullable T result) {
try { try {
@ -33,13 +40,17 @@ public class DonAsynchron {
} catch (Throwable th) { } catch (Throwable th) {
onFailure(th); onFailure(th);
} }
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
onFailure.accept(t); onFailure.accept(t);
} }
}); };
if (executor != null) {
Futures.addCallback(future, callback, executor);
} else {
Futures.addCallback(future, callback);
}
} }
} }

117
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<C extends TbAbstractAlarmNodeConfiguration> 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<AlarmResult> processAlarm(TbContext ctx, TbMsg msg);
protected ListenableFuture<JsonNode> 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;
}
}
}

26
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;
}

219
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java

@ -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 <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
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<Boolean> shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg));
ListenableFuture<AlarmResult> transform = Futures.transform(shouldCreate, (AsyncFunction<Boolean, AlarmResult>) 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<AlarmResult> createOrUpdate(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a == null || a.getStatus().isCleared()) {
return createNewAlarm(ctx, msg);
} else {
return updateAlarm(ctx, msg, a);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> checkForClearIfExist(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) 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<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg),
(Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
(Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
}
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) 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<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
}
private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Boolean> shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg));
return Futures.transform(shouldClear, (AsyncFunction<Boolean, AlarmResult>) clear -> {
if (clear) {
ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis());
return Futures.transform(clearFuture, (Function<Boolean, AlarmResult>) 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<JsonNode> 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;
}
}
}

80
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 <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeClearAlarmConfig",
icon = "notifications_off"
)
public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfiguration> {
@Override
protected TbClearAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbClearAlarmNodeConfiguration.class);
}
@Override
protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) 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<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<JsonNode> asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails());
return Futures.transform(asyncDetails, (AsyncFunction<JsonNode, AlarmResult>) details -> {
ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), details, System.currentTimeMillis());
return Futures.transform(clearFuture, (AsyncFunction<Boolean, AlarmResult>) 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());
}
}

36
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<TbClearAlarmNodeConfiguration> {
@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;
}
}

106
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 <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCreateAlarmConfig",
icon = "notifications_active"
)
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
@Override
protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
}
@Override
protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a == null || a.getStatus().isCleared()) {
return createNewAlarm(ctx, msg);
} else {
return updateAlarm(ctx, msg, a);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
(Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
(Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
}
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) 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<Alarm, AlarmResult>) 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();
}
}

19
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java → 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; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@Data @Data
public class TbAlarmNodeConfiguration implements NodeConfiguration { public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbCreateAlarmNodeConfiguration> {
private String createConditionJs;
private String clearConditionJs;
private String alarmDetailsBuildJs;
private String alarmType;
private AlarmSeverity severity; private AlarmSeverity severity;
private boolean propagate; private boolean propagate;
@Override @Override
public TbAlarmNodeConfiguration defaultConfiguration() { public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration(); TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;"); configuration.setAlarmDetailsBuildJs("var details = {};\n" +
configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;"); "if (metadata.prevAlarmDetails) {\n" +
configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;"); " details = JSON.parse(metadata.prevAlarmDetails);\n" +
"}\n"+
"return details;");
configuration.setAlarmType("General Alarm"); configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL); configuration.setSeverity(AlarmSeverity.CRITICAL);
configuration.setPropagate(false); configuration.setPropagate(false);

29
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<String> deviceTypes;
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -52,7 +52,7 @@ public class TbJsFilterNode implements TbNode {
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor(); ListeningExecutor jsExecutor = ctx.getJsExecutor();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)), withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
filterResult -> ctx.tellNext(msg, Boolean.toString(filterResult)), filterResult -> ctx.tellNext(msg, filterResult.booleanValue() ? "True" : "False"),
t -> ctx.tellError(msg, t)); t -> ctx.tellError(msg, t));
} }

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -26,8 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilter
@Override @Override
public TbJsFilterNodeConfiguration defaultConfiguration() { public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration(); TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' " + configuration.setJsScript("return msg.temperature > 20;");
"&& metadata.temp == 10 && msg.bigObj.prop == 42 && msgType === 'POST_TELEMETRY';");
return configuration; return configuration;
} }
} }

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

@ -31,8 +31,8 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitch
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration(); TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setJsScript("function nextRelation(metadata, msg) {\n" + configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
" return ['one','nine'];\n" + " return ['one','nine'];\n" +
"};\n" + "}\n" +
"if(msgType === 'POST_TELEMETRY') {\n" + "if(msgType === 'POST_TELEMETRY_REQUEST') {\n" +
" return ['two'];\n" + " return ['two'];\n" +
"}\n" + "}\n" +
"return nextRelation(metadata, msg);"); "return nextRelation(metadata, msg);");

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java

@ -45,7 +45,7 @@ public class TbMsgTypeFilterNode implements TbNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
ctx.tellNext(msg, Boolean.toString(config.getMessageTypes().contains(msg.getType()))); ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False");
} }
@Override @Override

103
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java

@ -0,0 +1,103 @@
/**
* 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.base.Function;
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.api.TbNode;
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.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
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.FAILURE;
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
import static org.thingsboard.server.common.data.DataConstants.*;
public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeConfiguration, T extends EntityId> 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<List<Void>> 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<Void> putAttrAsync(TbContext ctx, EntityId entityId, TbMsg msg, String scope, List<String> keys, String prefix) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, scope, keys);
return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, Void>) l -> {
l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString()));
return null;
});
}
private ListenableFuture<Void> putLatestTelemetry(TbContext ctx, EntityId entityId, TbMsg msg, List<String> keys) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, keys);
return Futures.transform(latest, (Function<? super List<TsKvEntry>, Void>) l -> {
l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString()));
return null;
});
}
@Override
public void destroy() {
}
protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
}

62
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; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.kv.TsKvEntry; 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.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. * Created by ashvayka on 19.01.18.
@ -47,50 +40,15 @@ import static org.thingsboard.server.common.data.DataConstants.*;
"<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ", "<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
public class TbGetAttributesNode implements TbNode { public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttributesNodeConfiguration, EntityId> {
private TbGetAttributesNodeConfiguration config;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { protected TbGetAttributesNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class); return TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
ListenableFuture<List<Void>> allFutures = Futures.allAsList( return Futures.immediateFuture(originator);
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<Void> putAttrAsync(TbContext ctx, TbMsg msg, String scope, List<String> keys, String prefix) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(msg.getOriginator(), scope, keys);
return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, Void>) l -> {
l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString()));
return null;
});
}
private ListenableFuture<Void> putLatestTelemetry(TbContext ctx, TbMsg msg, List<String> keys) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(msg.getOriginator(), keys);
return Futures.transform(latest, (Function<? super List<TsKvEntry>, Void>) l -> {
l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString()));
return null;
});
}
@Override
public void destroy() {
} }
} }

52
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, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
"with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into metadata without prefix. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeDeviceAttributesConfig")
public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDeviceAttrNodeConfiguration, DeviceId> {
@Override
protected TbGetDeviceAttrNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetDeviceAttrNodeConfiguration.class);
}
@Override
protected ListenableFuture<DeviceId> findEntityAsync(TbContext ctx, EntityId originator) {
return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, originator, config.getDeviceRelationsQuery());
}
}

48
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;
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java

@ -34,7 +34,7 @@ public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGet
Map<String, String> attrMapping = new HashMap<>(); Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo"); attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping); configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(true); configuration.setTelemetry(false);
return configuration; return configuration;
} }
} }

2
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<String, String> attrMapping = new HashMap<>(); Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo"); attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping); configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(true); configuration.setTelemetry(false);
RelationsQuery relationsQuery = new RelationsQuery(); RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM); relationsQuery.setDirection(EntitySearchDirection.FROM);

9
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.handler.ssl.SslContextBuilder;
import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import nl.jk5.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClient;
import nl.jk5.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import nl.jk5.mqtt.MqttConnectResult; import org.thingsboard.mqtt.MqttConnectResult;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.TbNodeUtils; import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*; import org.thingsboard.rule.engine.api.*;
@ -80,8 +80,7 @@ public class TbMqttNode implements TbNode {
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE) this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE)
.addListener(future -> { .addListener(future -> {
if (future.isSuccess()) { if (future.isSuccess()) {
TbMsg next = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData()); ctx.tellNext(msg, TbRelationTypes.SUCCESS);
ctx.tellNext(next, TbRelationTypes.SUCCESS);
} else { } else {
TbMsg next = processException(ctx, msg, future.cause()); TbMsg next = processException(ctx, msg, future.cause());
ctx.tellNext(next, TbRelationTypes.FAILURE, future.cause()); ctx.tellNext(next, TbRelationTypes.FAILURE, future.cause());

2
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; package org.thingsboard.rule.engine.mqtt.credentials;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import nl.jk5.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import java.util.Optional; import java.util.Optional;

2
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 io.netty.handler.ssl.SslContext;
import lombok.Data; import lombok.Data;
import nl.jk5.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import java.util.Optional; import java.util.Optional;

2
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 io.netty.handler.ssl.SslContextBuilder;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import nl.jk5.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMDecryptorProvider;

2
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.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContext;
import nl.jk5.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import java.util.Optional; import java.util.Optional;

2
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, routingKey,
properties, properties,
msg.getData().getBytes(UTF8)); 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) { private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) {

16
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())); ctx.tellError(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
return; 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(); String src = msg.getData();
TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src)); TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts);
Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData(); Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData();
if (tsKvMap == null) { if (tsKvMap == null) {
ctx.tellError(msg, new IllegalArgumentException("Msg body us empty: " + src)); ctx.tellError(msg, new IllegalArgumentException("Msg body is empty: " + src));
return; return;
} }
List<TsKvEntry> tsKvEntryList = new ArrayList<>(); List<TsKvEntry> tsKvEntryList = new ArrayList<>();

56
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<DeviceId> findDeviceAsync(TbContext ctx, EntityId originator,
DeviceRelationsQuery deviceRelationsQuery) {
DeviceService deviceService = ctx.getDeviceService();
DeviceSearchQuery query = buildQuery(originator, deviceRelationsQuery);
ListenableFuture<List<Device>> asyncDevices = deviceService.findDevicesByQuery(query);
return Futures.transform(asyncDevices, (AsyncFunction<List<Device>, 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;
}
}

6
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

98
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; package org.thingsboard.rule.engine.action;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -45,7 +46,7 @@ import java.util.concurrent.Callable;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*; 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.CRITICAL;
import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
import static org.thingsboard.server.common.data.alarm.AlarmStatus.*; 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) @RunWith(MockitoJUnitRunner.class)
public class TbAlarmNodeTest { public class TbAlarmNodeTest {
private TbAlarmNode node; private TbAbstractAlarmNode node;
@Mock @Mock
private TbContext ctx; private TbContext ctx;
@ -62,10 +63,6 @@ public class TbAlarmNodeTest {
@Mock @Mock
private AlarmService alarmService; private AlarmService alarmService;
@Mock
private ScriptEngine createJs;
@Mock
private ScriptEngine clearJs;
@Mock @Mock
private ScriptEngine detailsJs; private ScriptEngine detailsJs;
@ -100,11 +97,10 @@ public class TbAlarmNodeTest {
@Test @Test
public void newAlarmCanBeCreated() throws ScriptException, IOException { public void newAlarmCanBeCreated() throws ScriptException, IOException {
initWithScript(); initWithCreateAlarmScript();
metaData.putValue("key", "value"); metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); 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(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
@ -140,38 +136,15 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm); assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class)); verify(executor, times(1)).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);
} }
@Test @Test
public void buildDetailsThrowsException() throws ScriptException, IOException { public void buildDetailsThrowsException() throws ScriptException, IOException {
initWithScript(); initWithCreateAlarmScript();
metaData.putValue("key", "value"); metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); 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(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
@ -179,27 +152,24 @@ public class TbAlarmNodeTest {
verifyError(msg, "message", NotImplementedException.class); verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
verify(ctx).createJsScriptEngine("DETAILS", "Details"); verify(ctx).createJsScriptEngine("DETAILS", "Details");
verify(ctx, times(2)).getJsExecutor(); verify(ctx, times(1)).getJsExecutor();
verify(ctx).getAlarmService(); verify(ctx).getAlarmService();
verify(ctx, times(3)).getDbCallbackExecutor(); verify(ctx, times(2)).getDbCallbackExecutor();
verify(ctx).getTenantId(); verify(ctx).getTenantId();
verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType"); verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
verifyNoMoreInteractions(ctx, alarmService, clearJs); verifyNoMoreInteractions(ctx, alarmService);
} }
@Test @Test
public void ifAlarmClearedCreateNew() throws ScriptException, IOException { public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
initWithScript(); initWithCreateAlarmScript();
metaData.putValue("key", "value"); metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build(); Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenReturn(null); when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
@ -236,20 +206,18 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm); assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class)); verify(executor, times(1)).executeAsync(any(Callable.class));
} }
@Test @Test
public void alarmCanBeUpdated() throws ScriptException, IOException { public void alarmCanBeUpdated() throws ScriptException, IOException {
initWithScript(); initWithCreateAlarmScript();
metaData.putValue("key", "value"); metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
long oldEndDate = System.currentTimeMillis(); long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); 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(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
@ -287,23 +255,21 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm); assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class)); verify(executor, times(1)).executeAsync(any(Callable.class));
} }
@Test @Test
public void alarmCanBeCleared() throws ScriptException, IOException { public void alarmCanBeCleared() throws ScriptException, IOException {
initWithScript(); initWithClearAlarmScript();
metaData.putValue("key", "value"); metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
long oldEndDate = System.currentTimeMillis(); long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); 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(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); 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<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); // doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
@ -338,20 +304,16 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm); assertEquals(expectedAlarm, actualAlarm);
} }
private void initWithScript() { private void initWithCreateAlarmScript() {
try { try {
TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration(); TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration();
config.setPropagate(true); config.setPropagate(true);
config.setSeverity(CRITICAL); config.setSeverity(CRITICAL);
config.setAlarmType("SomeType"); config.setAlarmType("SomeType");
config.setCreateConditionJs("CREATE");
config.setClearConditionJs("CLEAR");
config.setAlarmDetailsBuildJs("DETAILS"); config.setAlarmDetailsBuildJs("DETAILS");
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); 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.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getTenantId()).thenReturn(tenantId);
@ -361,7 +323,31 @@ public class TbAlarmNodeTest {
mockJsExecutor(); 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); node.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) { } catch (TbNodeException ex) {
throw new IllegalStateException(ex); throw new IllegalStateException(ex);

4
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); node.onMsg(ctx, msg);
verify(ctx).getJsExecutor(); verify(ctx).getJsExecutor();
verify(ctx).tellNext(msg, "false"); verify(ctx).tellNext(msg, "False");
} }
@Test @Test
@ -88,7 +88,7 @@ public class TbJsFilterNodeTest {
node.onMsg(ctx, msg); node.onMsg(ctx, msg);
verify(ctx).getJsExecutor(); verify(ctx).getJsExecutor();
verify(ctx).tellNext(msg, "true"); verify(ctx).tellNext(msg, "True");
} }
private void initWithScript() throws TbNodeException { private void initWithScript() throws TbNodeException {

2
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); node.onMsg(ctx, msg);
verify(ctx).getJsExecutor(); verify(ctx).getJsExecutor();
ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class); ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), SUCCESS); verify(ctx).tellNext(captor.capture(), eq(SUCCESS));
TbMsg actualMsg = captor.getValue(); TbMsg actualMsg = captor.getValue();
assertEquals(transformedMsg, actualMsg); assertEquals(transformedMsg, actualMsg);
} }

4
ui/src/app/alarm/alarm-table.directive.js

@ -42,7 +42,7 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
history: { history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day timewindowMs: 24 * 60 * 60 * 1000 // 1 day
} }
} };
scope.topIndex = 0; scope.topIndex = 0;
@ -98,6 +98,8 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
} }
}; };
scope.reload = reload;
scope.$watch("entityId", function(newVal, prevVal) { scope.$watch("entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) { if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter(); resetFilter();

7
ui/src/app/alarm/alarm-table.tpl.html

@ -26,6 +26,13 @@
</md-select> </md-select>
</md-input-container> </md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow> <tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
<md-button ng-disabled="$root.loading"
class="md-icon-button" ng-click="reload()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</section> </section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1"> <div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table"> <md-list flex layout="column" class="tb-alarm-table">

14
ui/src/app/api/rule-chain.service.js

@ -34,7 +34,8 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
getRuleNodeComponentByClazz: getRuleNodeComponentByClazz, getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
getRuleNodeSupportedLinks: getRuleNodeSupportedLinks, getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
resolveTargetRuleChains: resolveTargetRuleChains, resolveTargetRuleChains: resolveTargetRuleChains,
testScript: testScript testScript: testScript,
getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput
}; };
return service; return service;
@ -313,4 +314,15 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise; 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;
}
} }

2
ui/src/app/event/event-header-debug-rulenode.tpl.html

@ -21,7 +21,7 @@
<div translate class="tb-cell" flex="15">event.entity</div> <div translate class="tb-cell" flex="15">event.entity</div>
<div translate class="tb-cell" flex="20">event.message-id</div> <div translate class="tb-cell" flex="20">event.message-id</div>
<div translate class="tb-cell" flex="20">event.message-type</div> <div translate class="tb-cell" flex="20">event.message-type</div>
<div translate class="tb-cell" flex="15">event.data-type</div> <div translate class="tb-cell" flex="15">event.relation-type</div>
<div translate class="tb-cell" flex="10">event.data</div> <div translate class="tb-cell" flex="10">event.data</div>
<div translate class="tb-cell" flex="10">event.metadata</div> <div translate class="tb-cell" flex="10">event.metadata</div>
<div translate class="tb-cell" flex="10">event.error</div> <div translate class="tb-cell" flex="10">event.error</div>

2
ui/src/app/event/event-row-debug-rulenode.tpl.html

@ -21,7 +21,7 @@
<div class="tb-cell" flex="15">{{event.body.entityName}}</div> <div class="tb-cell" flex="15">{{event.body.entityName}}</div>
<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div> <div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div> <div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
<div class="tb-cell" flex="15">{{event.body.dataType}}</div> <div class="tb-cell" flex="15">{{event.body.relationType}}</div>
<div class="tb-cell" flex="10"> <div class="tb-cell" flex="10">
<md-button ng-if="event.body.data" class="md-icon-button md-primary" <md-button ng-if="event.body.data" class="md-icon-button md-primary"
ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)" ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"

4
ui/src/app/locale/locale.constant.js

@ -782,6 +782,7 @@ export default angular.module('thingsboard.locale', [])
"message-id": "Message Id", "message-id": "Message Id",
"message-type": "Message Type", "message-type": "Message Type",
"data-type": "Data Type", "data-type": "Data Type",
"relation-type": "Relation Type",
"metadata": "Metadata", "metadata": "Metadata",
"data": "Data", "data": "Data",
"event": "Event", "event": "Event",
@ -1242,7 +1243,8 @@ export default angular.module('thingsboard.locale', [])
"metadata": "Metadata", "metadata": "Metadata",
"metadata-required": "Metadata entries can't be empty.", "metadata-required": "Metadata entries can't be empty.",
"output": "Output", "output": "Output",
"test": "Test" "test": "Test",
"help": "Help"
}, },
"rule-plugin": { "rule-plugin": {
"management": "Rules and plugins management" "management": "Rules and plugins management"

2
ui/src/app/rulechain/rulechain.controller.js

@ -489,7 +489,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
function displayNodeDescriptionTooltip(event, node) { function displayNodeDescriptionTooltip(event, node) {
displayTooltip(event, displayTooltip(event,
'<div class="tb-rule-node-tooltip">' + '<div class="tb-rule-node-tooltip">' +
'<div id="tooltip-content" layout="column">' + '<div id="tb-node-content" layout="column">' +
'<div class="tb-node-title">' + node.component.name + '</div>' + '<div class="tb-node-title">' + node.component.name + '</div>' +
'<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' + '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
'<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' + '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +

13
ui/src/app/rulechain/rulechain.scss

@ -431,10 +431,17 @@
} }
} }
.tb-rule-node-tooltip, .tb-rule-node-help {
color: #333;
}
.tb-rule-node-tooltip { .tb-rule-node-tooltip {
font-size: 14px; font-size: 14px;
width: 300px; width: 300px;
color: #333; }
.tb-rule-node-help {
font-size: 16px;
} }
.tb-rule-node-error-tooltip { .tb-rule-node-error-tooltip {
@ -442,8 +449,8 @@
color: #ea0d0d; color: #ea0d0d;
} }
.tb-rule-node-tooltip, .tb-rule-node-error-tooltip { .tb-rule-node-tooltip, .tb-rule-node-error-tooltip, .tb-rule-node-help {
#tooltip-content { #tb-node-content {
.tb-node-title { .tb-node-title {
font-weight: 600; font-weight: 600;
} }

11
ui/src/app/rulechain/rulechain.tpl.html

@ -178,6 +178,17 @@
default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}"> default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
</tb-event-table> </tb-event-table>
</md-tab> </md-tab>
<md-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">
<div id="tb-node-content" class="md-padding" layout="column">
<div class="tb-node-title">{{vm.editingRuleNode.component.name}}</div>
<div>&nbsp;</div>
<div class="tb-node-description">{{vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
<div>&nbsp;</div>
<div class="tb-node-details" ng-bind-html="vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
</div>
</div>
</md-tab>
</md-tabs> </md-tabs>
</tb-details-sidenav> </tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav" <tb-details-sidenav class="tb-rulenode-link-details-sidenav"

1
ui/src/app/rulechain/rulenode-config.directive.js

@ -68,6 +68,7 @@ export default function RuleNodeConfigDirective($compile, $templateCache, $injec
restrict: "E", restrict: "E",
require: "^ngModel", require: "^ngModel",
scope: { scope: {
ruleNodeId:'=',
nodeDefinition:'=', nodeDefinition:'=',
required:'=ngRequired', required:'=ngRequired',
readonly:'=ngReadonly' readonly:'=ngReadonly'

1
ui/src/app/rulechain/rulenode-config.tpl.html

@ -17,6 +17,7 @@
--> -->
<tb-rule-node-defined-config ng-if="useDefinedDirective()" <tb-rule-node-defined-config ng-if="useDefinedDirective()"
rule-node-id="ruleNodeId"
ng-model="configuration" ng-model="configuration"
rule-node-directive="{{nodeDefinition.configDirective}}" rule-node-directive="{{nodeDefinition.configDirective}}"
ng-required="required" ng-required="required"

3
ui/src/app/rulechain/rulenode-defined-config.directive.js

@ -40,7 +40,7 @@ export default function RuleNodeDefinedConfigDirective($compile) {
scope.ruleNodeConfigScope.$destroy(); scope.ruleNodeConfigScope.$destroy();
} }
var directive = snake_case(attrs.ruleNodeDirective, '-'); var directive = snake_case(attrs.ruleNodeDirective, '-');
var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`; var template = `<${directive} rule-node-id="ruleNodeId" ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
element.html(template); element.html(template);
scope.ruleNodeConfigScope = scope.$new(); scope.ruleNodeConfigScope = scope.$new();
$compile(element.contents())(scope.ruleNodeConfigScope); $compile(element.contents())(scope.ruleNodeConfigScope);
@ -58,6 +58,7 @@ export default function RuleNodeDefinedConfigDirective($compile) {
restrict: "E", restrict: "E",
require: "^ngModel", require: "^ngModel",
scope: { scope: {
ruleNodeId:'=',
required:'=ngRequired', required:'=ngRequired',
readonly:'=ngReadonly' readonly:'=ngReadonly'
}, },

1
ui/src/app/rulechain/rulenode-fieldset.tpl.html

@ -37,6 +37,7 @@
</md-input-container> </md-input-container>
</section> </section>
<tb-rule-node-config ng-model="ruleNode.configuration" <tb-rule-node-config ng-model="ruleNode.configuration"
rule-node-id="ruleNode.ruleNodeId.id"
ng-required="true" ng-required="true"
node-definition="ruleNode.component.configurationDescriptor.nodeDefinition" node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
ng-readonly="$root.loading || !isEdit || isReadOnly"> ng-readonly="$root.loading || !isEdit || isReadOnly">

67
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 */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function NodeScriptTest($q, $mdDialog, $document) { export default function NodeScriptTest($q, $mdDialog, $document, ruleChainService) {
var service = { var service = {
testNodeScript: testNodeScript testNodeScript: testNodeScript
@ -29,12 +29,72 @@ export default function NodeScriptTest($q, $mdDialog, $document) {
return service; 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(); var deferred = $q.defer();
if ($event) { if ($event) {
$event.stopPropagation(); $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 = { var onShowingCallback = {
onShowed: () => { onShowed: () => {
} }
@ -74,7 +134,6 @@ export default function NodeScriptTest($q, $mdDialog, $document) {
deferred.reject(); deferred.reject();
} }
); );
return deferred.promise; return deferred.promise;
} }

2
ui/src/app/rulechain/script/node-script-test.tpl.html

@ -38,7 +38,7 @@
<ng-form name="payloadForm"> <ng-form name="payloadForm">
<div layout="column" style="height: 100%;"> <div layout="column" style="height: 100%;">
<div layout="row"> <div layout="row">
<md-input-container class="md-block" style="margin-bottom: 0px; min-width: 200px;"> <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 300px;">
<label translate>rulenode.message-type</label> <label translate>rulenode.message-type</label>
<input required name="msgType" ng-model="vm.inputParams.msgType"> <input required name="msgType" ng-model="vm.inputParams.msgType">
<div ng-messages="payloadForm.msgType.$error"> <div ng-messages="payloadForm.msgType.$error">

Loading…
Cancel
Save